6d65f7eb09
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.
163 lines
6.8 KiB
Plaintext
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
|