diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ddd986a..685a262f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,10 @@ jobs: - 'src/**' - 'Cargo.toml' - 'Cargo.lock' + - 'tests/api/**' + - 'tests/e2e/**' + - 'tests/fixtures/**' + - 'tests/common/**' frontend-check: name: Frontend — CSS and JS checks (format, lint, rules) diff --git a/tests/api/README.md b/tests/api/README.md index 62be5dea..f3347c37 100644 --- a/tests/api/README.md +++ b/tests/api/README.md @@ -44,9 +44,95 @@ hurl --variables-file tests/api/test.env --test --report-html /tmp/hurl-report t | File | Description | |---|---| | `setup.hurl` | One-time admin account creation; also asserts the endpoint is locked afterwards | -| `contacts.hurl` | Full contacts CRUD scenario (13 steps, see below) | +| `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 | diff --git a/tests/api/favorites.hurl b/tests/api/favorites.hurl new file mode 100644 index 00000000..83976e4f --- /dev/null +++ b/tests/api/favorites.hurl @@ -0,0 +1,161 @@ +# ============================================================= +# OxiCloud – Favorites API end-to-end scenario +# ============================================================= +# Depends on files-folders.hurl having run first: +# - home folder exists with sub-folders test1 and test2-renamed +# - test2-renamed contains hello-renamed.txt +# +# Run: +# hurl --variables-file tests/api/test.env --test tests/api/favorites.hurl +# ============================================================= + + +# ───────────────────────────────────────────────────────────── +# 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 + + +# ───────────────────────────────────────────────────────────── +# Step 2 – No favorites yet +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/favorites +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 3 – Discover item IDs from the folder structure +# Folders are ORDER BY name: test1 ($[0]), test2-renamed ($[1]) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +home_folder_id: jsonpath "$[0].id" + + +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +test1_id: jsonpath "$[0].id" +test2_id: jsonpath "$[1].id" +[Asserts] +jsonpath "$[0].name" == "test1" +jsonpath "$[1].name" == "test2-renamed" + + +GET {{base_url}}/api/files?folder_id={{test2_id}} +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +file_id: jsonpath "$[0].id" +[Asserts] +jsonpath "$[0].name" == "hello-renamed.txt" + + +# ───────────────────────────────────────────────────────────── +# Step 4 – Add hello-renamed.txt to favorites +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/favorites/file/{{file_id}} +Authorization: Bearer {{token}} + +HTTP 201 + + +# ───────────────────────────────────────────────────────────── +# Step 5 – Favorites contains only hello-renamed.txt +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/favorites +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].item_id" == {{file_id}} +jsonpath "$[0].item_type" == "file" +jsonpath "$[0].item_name" == "hello-renamed.txt" + + +# ───────────────────────────────────────────────────────────── +# Step 6 – Add test1 folder to favorites +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/favorites/folder/{{test1_id}} +Authorization: Bearer {{token}} + +HTTP 201 + + +# ───────────────────────────────────────────────────────────── +# Step 7 – Favorites contains both items (order-independent) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/favorites +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 2 +jsonpath "$[*].item_id" contains {{file_id}} +jsonpath "$[*].item_id" contains {{test1_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 8 – Remove hello-renamed.txt from favorites +# ───────────────────────────────────────────────────────────── +DELETE {{base_url}}/api/favorites/file/{{file_id}} +Authorization: Bearer {{token}} + +HTTP 200 + + +# ───────────────────────────────────────────────────────────── +# Step 9 – Favorites contains only test1 folder +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/favorites +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].item_id" == {{test1_id}} +jsonpath "$[0].item_type" == "folder" +jsonpath "$[0].item_name" == "test1" + + +# ───────────────────────────────────────────────────────────── +# Step 10 – Cleanup: remove test1 from favorites +# ───────────────────────────────────────────────────────────── +DELETE {{base_url}}/api/favorites/folder/{{test1_id}} +Authorization: Bearer {{token}} + +HTTP 200 + + +# ───────────────────────────────────────────────────────────── +# Step 11 – Favorites is empty again +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/favorites +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 0 diff --git a/tests/api/files-folders.hurl b/tests/api/files-folders.hurl new file mode 100644 index 00000000..ab9a4fa1 --- /dev/null +++ b/tests/api/files-folders.hurl @@ -0,0 +1,344 @@ +# ============================================================= +# OxiCloud – Files & Folders API end-to-end scenario +# ============================================================= +# Run: +# hurl --variables-file tests/api/test.env --test tests/api/files-folders.hurl +# ============================================================= + + +# ───────────────────────────────────────────────────────────── +# 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 root folders +# A fresh admin account has exactly one root folder +# (the home folder created at setup time). +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +home_folder_id: jsonpath "$[0].id" +home_folder_name: jsonpath "$[0].name" +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 1 +jsonpath "$[0].id" isString +jsonpath "$[0].name" isString +jsonpath "$[0].parent_id" == null + + +# ───────────────────────────────────────────────────────────── +# Step 3 – Browse the home folder: sub-folders must be empty +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 4 – Browse the home folder: files must be empty +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files?folder_id={{home_folder_id}} +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 5 – Creating a folder named "/" must be rejected (400) +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/folders +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "/", + "parent_id": "{{home_folder_id}}" +} + +HTTP 400 +[Asserts] +jsonpath "$.error_type" == "Invalid Input" + + +# ───────────────────────────────────────────────────────────── +# Step 6 – Create folder test1 inside the home folder +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/folders +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "test1", + "parent_id": "{{home_folder_id}}" +} + +HTTP 201 +[Captures] +test1_id: jsonpath "$.id" +[Asserts] +jsonpath "$.id" isString +jsonpath "$.name" == "test1" +jsonpath "$.parent_id" == {{home_folder_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 7 – Create folder test2 inside test1 +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/folders +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "test2", + "parent_id": "{{test1_id}}" +} + +HTTP 201 +[Captures] +test2_id: jsonpath "$.id" +[Asserts] +jsonpath "$.id" isString +jsonpath "$.name" == "test2" +jsonpath "$.parent_id" == {{test1_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 8 – Home folder now has exactly one sub-folder (test1) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].id" == {{test1_id}} +jsonpath "$[0].name" == "test1" + + +# ───────────────────────────────────────────────────────────── +# Step 9 – test1 has exactly one sub-folder (test2) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{test1_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].id" == {{test2_id}} +jsonpath "$[0].name" == "test2" +jsonpath "$[0].parent_id" == {{test1_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 10 – Upload hello.txt into test2 +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/files/upload +Authorization: Bearer {{token}} +[MultipartFormData] +folder_id: {{test2_id}} +file: file,fixtures/hello.txt; text/plain + +HTTP 201 +[Captures] +file_id: jsonpath "$.id" +[Asserts] +jsonpath "$.id" isString +jsonpath "$.name" == "hello.txt" +jsonpath "$.folder_id" == {{test2_id}} +jsonpath "$.size" == 21 +jsonpath "$.mime_type" == "text/plain" + + +# ───────────────────────────────────────────────────────────── +# Step 11 – test2 contains exactly hello.txt; verify mime and icon +# icon_class is only available in the full FileDto +# returned by the listing endpoint +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files?folder_id={{test2_id}} +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].id" == {{file_id}} +jsonpath "$[0].name" == "hello.txt" +jsonpath "$[0].mime_type" == "text/plain" +jsonpath "$[0].icon_class" == "fas fa-file-alt" + + +# ───────────────────────────────────────────────────────────── +# Step 12 – Move test2 from test1 into the home folder +# ───────────────────────────────────────────────────────────── +PUT {{base_url}}/api/folders/{{test2_id}}/move +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "parent_id": "{{home_folder_id}}" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == {{test2_id}} +jsonpath "$.name" == "test2" +jsonpath "$.parent_id" == {{home_folder_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 13 – test1 is now empty (test2 was its only child) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{test1_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 14 – Home folder now has two sub-folders: test1 and test2 +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 2 +jsonpath "$[*].id" contains {{test1_id}} +jsonpath "$[*].id" contains {{test2_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 15 – Rename test2 to test2-renamed +# ───────────────────────────────────────────────────────────── +PUT {{base_url}}/api/folders/{{test2_id}}/rename +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "test2-renamed" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == {{test2_id}} +jsonpath "$.name" == "test2-renamed" +jsonpath "$.parent_id" == {{home_folder_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 16 – Rename hello.txt to hello-renamed.txt +# ───────────────────────────────────────────────────────────── +PUT {{base_url}}/api/files/{{file_id}}/rename +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "hello-renamed.txt" +} + +HTTP 200 +[Asserts] +jsonpath "$.id" == {{file_id}} +jsonpath "$.name" == "hello-renamed.txt" +jsonpath "$.folder_id" == {{test2_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 17 – Renaming a file to "." must be rejected (400) +# ───────────────────────────────────────────────────────────── +PUT {{base_url}}/api/files/{{file_id}}/rename +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "." +} + +HTTP 400 +[Asserts] +jsonpath "$.error_type" == "Invalid Input" + + +# ───────────────────────────────────────────────────────────── +# Step 18 – Upload oxicloud-logo.jpg into the home folder +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/files/upload +Authorization: Bearer {{token}} +[MultipartFormData] +folder_id: {{home_folder_id}} +file: file,fixtures/oxicloud-logo.jpg; image/jpeg + +HTTP 201 +[Captures] +logo_id: jsonpath "$.id" +[Asserts] +jsonpath "$.name" == "oxicloud-logo.jpg" +jsonpath "$.mime_type" == "image/jpeg" +jsonpath "$.folder_id" == {{home_folder_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 19 – Home folder file listing: verify icon class for JPEG +# (icon_class is only present in the listing response) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files?folder_id={{home_folder_id}} +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].name" == "oxicloud-logo.jpg" +jsonpath "$[0].mime_type" == "image/jpeg" +jsonpath "$[0].icon_class" == "fas fa-file-image" + + +# ───────────────────────────────────────────────────────────── +# Step 20 – Thumbnail icon size exists (200 with image bytes) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files/{{logo_id}}/thumbnail/icon +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +header "Content-Type" startsWith "image/" + + +# ───────────────────────────────────────────────────────────── +# Step 21 – Thumbnail preview size exists +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files/{{logo_id}}/thumbnail/preview +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +header "Content-Type" startsWith "image/" + + +# ───────────────────────────────────────────────────────────── +# Step 22 – Thumbnail large size exists +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files/{{logo_id}}/thumbnail/large +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +header "Content-Type" startsWith "image/" diff --git a/tests/api/recent.hurl b/tests/api/recent.hurl new file mode 100644 index 00000000..2fd5c08b --- /dev/null +++ b/tests/api/recent.hurl @@ -0,0 +1,102 @@ +# ============================================================= +# OxiCloud – Recent items API end-to-end scenario +# ============================================================= +# Depends on files-folders.hurl having run first: +# - home folder exists with test1 and test2-renamed +# - test2-renamed contains hello-renamed.txt +# +# Run: +# hurl --variables-file tests/api/test.env --test tests/api/recent.hurl +# ============================================================= + + +# ───────────────────────────────────────────────────────────── +# 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 + + +# ───────────────────────────────────────────────────────────── +# Step 2 – Discover the file ID of hello-renamed.txt +# Folders are ORDER BY name: test1 ($[0]), test2-renamed ($[1]) +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +home_folder_id: jsonpath "$[0].id" + + +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +test2_id: jsonpath "$[1].id" +[Asserts] +jsonpath "$[1].name" == "test2-renamed" + + +GET {{base_url}}/api/files?folder_id={{test2_id}} +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +file_id: jsonpath "$[0].id" +[Asserts] +jsonpath "$[0].name" == "hello-renamed.txt" + + +# ───────────────────────────────────────────────────────────── +# Step 3 – Record access to hello-renamed.txt +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/recent/file/{{file_id}} +Authorization: Bearer {{token}} + +HTTP 200 + + +# ───────────────────────────────────────────────────────────── +# Step 4 – Recent list contains hello-renamed.txt +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/recent +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].item_type" == "file" +jsonpath "$[0].item_name" == "hello-renamed.txt" + + +# ───────────────────────────────────────────────────────────── +# Step 5 – Clear all recent items +# ───────────────────────────────────────────────────────────── +DELETE {{base_url}}/api/recent/clear +Authorization: Bearer {{token}} + +HTTP 200 + + +# ───────────────────────────────────────────────────────────── +# Step 6 – Recent list is empty after clear +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/recent +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 0 diff --git a/tests/api/run.sh b/tests/api/run.sh index 4a888146..e5485858 100755 --- a/tests/api/run.sh +++ b/tests/api/run.sh @@ -85,8 +85,12 @@ log "Server is ready." # ── 4. Run Hurl tests ───────────────────────────────────────────────────────── log "Running Hurl tests..." -hurl --variables-file "$API_DIR/test.env" --test --jobs 1 \ +hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test --jobs 1 \ "$API_DIR/setup.hurl" \ + "$API_DIR/files-folders.hurl" \ + "$API_DIR/favorites.hurl" \ + "$API_DIR/trash.hurl" \ + "$API_DIR/recent.hurl" \ "$API_DIR/contacts.hurl" log "All tests passed." diff --git a/tests/api/trash.hurl b/tests/api/trash.hurl new file mode 100644 index 00000000..6659778b --- /dev/null +++ b/tests/api/trash.hurl @@ -0,0 +1,206 @@ +# ============================================================= +# OxiCloud – Trash API end-to-end scenario +# ============================================================= +# Depends on files-folders.hurl having run first (home folder +# with test1 and test2-renamed must exist). +# +# Run: +# hurl --variables-file tests/api/test.env --test tests/api/trash.hurl +# ============================================================= + + +# ───────────────────────────────────────────────────────────── +# 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 + + +# ───────────────────────────────────────────────────────────── +# Step 2 – Trash is empty at the start +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/trash +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" isCollection +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 3 – Capture the home folder ID +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +home_folder_id: jsonpath "$[0].id" + + +# ───────────────────────────────────────────────────────────── +# Step 4 – Create "to-delete" folder inside the home folder +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/folders +Authorization: Bearer {{token}} +Content-Type: application/json +{ + "name": "to-delete", + "parent_id": "{{home_folder_id}}" +} + +HTTP 201 +[Captures] +to_delete_id: jsonpath "$.id" +[Asserts] +jsonpath "$.name" == "to-delete" +jsonpath "$.parent_id" == {{home_folder_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 5 – Upload hello.txt into "to-delete" +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/files/upload +Authorization: Bearer {{token}} +[MultipartFormData] +folder_id: {{to_delete_id}} +file: file,fixtures/hello.txt; text/plain + +HTTP 201 +[Asserts] +jsonpath "$.name" == "hello.txt" +jsonpath "$.folder_id" == {{to_delete_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 6 – Delete "to-delete" folder (moves it to trash) +# ───────────────────────────────────────────────────────────── +DELETE {{base_url}}/api/folders/{{to_delete_id}} +Authorization: Bearer {{token}} + +HTTP 204 + + +# ───────────────────────────────────────────────────────────── +# Step 7 – Trash contains exactly the "to-delete" folder +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/trash +Authorization: Bearer {{token}} + +HTTP 200 +[Captures] +trash_id: jsonpath "$[0].id" +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].item_type" == "folder" +jsonpath "$[0].name" == "to-delete" +jsonpath "$[0].original_id" == {{to_delete_id}} + + +# ───────────────────────────────────────────────────────────── +# Step 8 – "to-delete" is no longer listed in the home folder +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$[*].name" not contains "to-delete" + + +# ───────────────────────────────────────────────────────────── +# Step 9 – Restore "to-delete" from trash +# ───────────────────────────────────────────────────────────── +POST {{base_url}}/api/trash/{{trash_id}}/restore +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$.success" == true + + +# ───────────────────────────────────────────────────────────── +# Step 10 – "to-delete" folder is back in the home folder +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$[*].name" contains "to-delete" + + +# ───────────────────────────────────────────────────────────── +# Step 11 – hello.txt is still inside the restored folder +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/files?folder_id={{to_delete_id}} +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 1 +jsonpath "$[0].name" == "hello.txt" + + +# ───────────────────────────────────────────────────────────── +# Step 12 – Trash is empty after restore +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/trash +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 13 – Delete "to-delete" folder again (moves to trash) +# ───────────────────────────────────────────────────────────── +DELETE {{base_url}}/api/folders/{{to_delete_id}} +Authorization: Bearer {{token}} + +HTTP 204 + + +# ───────────────────────────────────────────────────────────── +# Step 14 – Empty the trash +# ───────────────────────────────────────────────────────────── +DELETE {{base_url}}/api/trash/empty +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$.success" == true + + +# ───────────────────────────────────────────────────────────── +# Step 15 – Trash is empty after purge +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/trash +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$" count == 0 + + +# ───────────────────────────────────────────────────────────── +# Step 16 – "to-delete" folder no longer exists in home folder +# ───────────────────────────────────────────────────────────── +GET {{base_url}}/api/folders/{{home_folder_id}}/contents +Authorization: Bearer {{token}} + +HTTP 200 +[Asserts] +jsonpath "$[*].name" not contains "to-delete" diff --git a/tests/fixtures/hello.txt b/tests/fixtures/hello.txt new file mode 100644 index 00000000..82af399e --- /dev/null +++ b/tests/fixtures/hello.txt @@ -0,0 +1 @@ +hello from hurl test diff --git a/tests/fixtures/oxicloud-logo.jpg b/tests/fixtures/oxicloud-logo.jpg new file mode 100644 index 00000000..0a1b8907 Binary files /dev/null and b/tests/fixtures/oxicloud-logo.jpg differ