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.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# =============================================================
|
||||
# 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
|
||||
Reference in New Issue
Block a user