Files
Oxicloud/tests/api/search_basic.hurl
T
Edouard Vanbelle 6d65f7eb09 chore(test): add new API coverage
ensure better API test coverage on important routes

  1. tests/api/public_shares.hurl — create a share token, verify, list contents, fetch a file, fetch a folder zip, then revoke and re-verify with the token. Same pattern as grants.hurl. ~30 min, biggest security ROI.
  2. tests/api/auth_session_lifecycle.hurl — login → refresh → use new token → logout → refresh-rejected → login-again. Covers the session-family invalidation contract.
  3. tests/api/admin_user_ops.hurl — admin disables / re-enables / changes role / resets password / sets quota for a fixture user. Five POSTs.
  4. tests/api/groups_effective_members.hurl — nested groups: A contains B contains user X; effective-members returns X. Two scenarios, but it's the ReBAC contract under the Drive refactor.
  5. tests/api/search_basic.hurl — upload foo.txt, search "foo", get the result; cross-user: bob can't search alice's foo.
2026-06-13 19:03:07 +02:00

163 lines
6.8 KiB
Plaintext

# =============================================================
# OxiCloud — Baseline: search surface
# =============================================================
# Pins `/api/search` and `/api/search/suggest` plus the
# cross-user isolation property: a search MUST NEVER surface a
# file the caller doesn't own (and isn't shared with). Search
# is the kind of feature where a sloppy SQL join is exactly
# what introduces a cross-user leak — this test catches that.
#
# Requires OXICLOUD_ENABLE_SEARCH=true (set in tests/common/server.env).
#
# Coverage:
# 1. Admin uploads `unique-search-needle-aaa.txt` to her home
# 2. GET /api/search?query=unique-search-needle returns the file
# 3. GET /api/search?query=does-not-exist-xyz returns 0 files
# 4. GET /api/search/suggest?query=unique-search-needle returns
# something (suggestion-shape is allowed to be permissive)
# 5. Cross-user: bob searches "unique-search-needle" → MUST NOT
# see admin's file (security baseline)
# 6. Teardown: delete the file
#
# Bob is (re-)created inline so this file is order-independent
# with respect to nc_second_user_setup.hurl (which runs later
# in run.sh).
# =============================================================
# ─────────────────────────────────────────────────────────────
# Setup — admin login + bob (re-)provisioning
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
admin_token: jsonpath "$.access_token"
GET {{base_url}}/api/folders
Authorization: Bearer {{admin_token}}
HTTP 200
[Captures]
admin_home_id: jsonpath "$[0].id"
# Anti-enum registration: 200 whether bob existed or not.
POST {{base_url}}/api/auth/register
Content-Type: application/json
{
"username": "bob",
"email": "bob@example.com",
"password": "BobPassword1!"
}
HTTP 200
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "bob", "password": "BobPassword1!" }
HTTP 200
[Captures]
bob_token: jsonpath "$.access_token"
# ─────────────────────────────────────────────────────────────
# 1 — Admin uploads `hello.txt` to a dedicated subfolder, then
# renames it to a deliberately unique name so the search
# assertion is unambiguous. The subfolder isolates this
# test from any other test that already left a `hello.txt`
# in admin's home (would otherwise 409).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/folders
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{ "name": "search-basic-test", "parent_id": "{{admin_home_id}}" }
HTTP 201
[Captures]
search_folder_id: jsonpath "$.id"
POST {{base_url}}/api/files/upload
Authorization: Bearer {{admin_token}}
[MultipartFormData]
folder_id: {{search_folder_id}}
file: file,fixtures/hello.txt; text/plain
HTTP 201
[Captures]
needle_file_id: jsonpath "$.id"
PUT {{base_url}}/api/files/{{needle_file_id}}/rename
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{ "name": "unique-search-needle-aaa.txt" }
HTTP 200
# ─────────────────────────────────────────────────────────────
# 2 — Search hits the seeded file by substring of its name.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/search?query=unique-search-needle
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.files" count >= 1
body contains "{{needle_file_id}}"
# ─────────────────────────────────────────────────────────────
# 3 — A search for a phrase that can't match anything must
# return an empty result set, NOT an error. Empty-results
# is a hot path; we don't want it to start 500ing.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/search?query=does-not-exist-xyz-zzz-9999
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.files" count == 0
jsonpath "$.folders" count == 0
# ─────────────────────────────────────────────────────────────
# 4 — Suggest returns a usable payload (shape is permissive —
# just confirm the endpoint serves 200 and isn't truncating
# to an error envelope).
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/search/suggest?query=unique-search-needle
Authorization: Bearer {{admin_token}}
HTTP 200
# ─────────────────────────────────────────────────────────────
# 5 — HEADLINE: bob MUST NOT see admin's file. If this assertion
# ever flips, the search service has a cross-user leak.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/search?query=unique-search-needle
Authorization: Bearer {{bob_token}}
HTTP 200
[Asserts]
body not contains "unique-search-needle"
body not contains "{{needle_file_id}}"
# ─────────────────────────────────────────────────────────────
# 6 — Teardown: removing the folder recursively takes the file
# with it, so a single DELETE is enough.
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/api/folders/{{search_folder_id}}
Authorization: Bearer {{admin_token}}
HTTP 204