test(job-registry): additional hurl coverage
This commit is contained in:
@@ -0,0 +1,157 @@
|
|||||||
|
# =============================================================
|
||||||
|
# OxiCloud — JobRegistry admin surface
|
||||||
|
# =============================================================
|
||||||
|
# Pins `GET /api/admin/jobs` and `POST /api/admin/jobs/{name}/trigger`
|
||||||
|
# — the production admin surface for the periodic-job scheduler
|
||||||
|
# (`docs/plan/job-registry.md` Part 1).
|
||||||
|
#
|
||||||
|
# Coverage:
|
||||||
|
# * Listing returns the four registered tenants
|
||||||
|
# (trash_cleanup, storage_reconcile, dedup_gc, grant_cleanup).
|
||||||
|
# * Scheduled jobs report `interval_ms`; on-demand jobs
|
||||||
|
# (`dedup_gc`) omit it via `skip_serializing_if=None`.
|
||||||
|
# * Triggering a job updates its `last_outcome` in the next list.
|
||||||
|
# * Unknown job → 404 (anti-enum on the trigger URL).
|
||||||
|
# * Non-admin caller → 403 from the admin middleware layer
|
||||||
|
# (no bespoke check in `list_jobs`/`trigger_job` itself).
|
||||||
|
# =============================================================
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Setup — admin login + jobs_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"
|
||||||
|
|
||||||
|
|
||||||
|
# Anti-enum registration.
|
||||||
|
POST {{base_url}}/api/auth/register
|
||||||
|
Content-Type: application/json
|
||||||
|
{
|
||||||
|
"username": "jobs_bob",
|
||||||
|
"email": "jobs_bob@example.com",
|
||||||
|
"password": "JobsBobPassword1!"
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTP 200
|
||||||
|
|
||||||
|
|
||||||
|
POST {{base_url}}/api/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
{ "username": "jobs_bob", "password": "JobsBobPassword1!" }
|
||||||
|
|
||||||
|
HTTP 200
|
||||||
|
[Captures]
|
||||||
|
bob_token: jsonpath "$.access_token"
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Step 1 — Admin lists jobs. All four Part 1 tenants must appear.
|
||||||
|
# Scheduled jobs (trash_cleanup, storage_reconcile, grant_cleanup)
|
||||||
|
# report `interval_ms`; on-demand jobs (`dedup_gc`) omit it via
|
||||||
|
# serde's `skip_serializing_if = "Option::is_none"`.
|
||||||
|
#
|
||||||
|
# JSONPath idiom (per `feedback_hurl_jsonpath_filter_empty`): the
|
||||||
|
# single-match `[?(...)]` filter unwraps to a scalar so `count`
|
||||||
|
# fails; `count == 0` also fails on the zero-match case ("no
|
||||||
|
# value"). Aggregate `$..field` + `contains` is the reliable
|
||||||
|
# primitive Ed's memory endorses.
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
GET {{base_url}}/api/admin/jobs
|
||||||
|
Authorization: Bearer {{admin_token}}
|
||||||
|
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
# Names — the four scheduler tenants.
|
||||||
|
jsonpath "$[*].name" contains "trash_cleanup"
|
||||||
|
jsonpath "$[*].name" contains "storage_reconcile"
|
||||||
|
jsonpath "$[*].name" contains "dedup_gc"
|
||||||
|
jsonpath "$[*].name" contains "grant_cleanup"
|
||||||
|
|
||||||
|
# Scheduled jobs' interval_ms values, in whatever order:
|
||||||
|
# TrashCleanup → 24 h = 86_400_000 ms
|
||||||
|
# GrantCleanup → 24 h = 86_400_000 ms
|
||||||
|
# StorageReconcile → 600 s = 600_000 ms
|
||||||
|
# Recursive descent collects all interval_ms values across the
|
||||||
|
# array; `contains` doesn't care about order.
|
||||||
|
jsonpath "$..interval_ms" contains 86400000
|
||||||
|
jsonpath "$..interval_ms" contains 600000
|
||||||
|
|
||||||
|
# On-demand job (`dedup_gc`) has no interval_ms field, so the
|
||||||
|
# total count of interval_ms values is 3, not 4. Combined with
|
||||||
|
# the four-name check above, this pins the on-demand-omission
|
||||||
|
# behaviour without hitting the single-match filter trap.
|
||||||
|
jsonpath "$..interval_ms" count == 3
|
||||||
|
|
||||||
|
# Every entry carries a `running` bool — same aggregate primitive.
|
||||||
|
jsonpath "$..running" count == 4
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Step 3 — Trigger `trash_cleanup`. Envelope shape:
|
||||||
|
# `{ ok, outcome: { outcome, count, extra } }`.
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
POST {{base_url}}/api/admin/jobs/trash_cleanup/trigger
|
||||||
|
Authorization: Bearer {{admin_token}}
|
||||||
|
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$.ok" == true
|
||||||
|
jsonpath "$.outcome.outcome" == "ok"
|
||||||
|
# Trash may or may not have expired items; count is a non-negative
|
||||||
|
# integer either way. `isNumber` on a 1-element JSONPath extract
|
||||||
|
# needs the filter idiom too — assert exists via the wrapper key.
|
||||||
|
jsonpath "$.outcome.count" exists
|
||||||
|
jsonpath "$.outcome.extra.files_purged" exists
|
||||||
|
jsonpath "$.outcome.extra.folders_purged" exists
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Step 4 — Re-list. At least one job (`trash_cleanup`, just
|
||||||
|
# triggered above) now has a `last_outcome` populated
|
||||||
|
# with `outcome=ok`. Aggregate JSONPath — recursive
|
||||||
|
# descent collects every last_outcome.outcome value
|
||||||
|
# across the response.
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
GET {{base_url}}/api/admin/jobs
|
||||||
|
Authorization: Bearer {{admin_token}}
|
||||||
|
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$..last_outcome.outcome" contains "ok"
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Step 5 — Trigger a job that doesn't exist. 404 anti-enum on
|
||||||
|
# `JobRegistry::trigger` returning `None`.
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
POST {{base_url}}/api/admin/jobs/no_such_job/trigger
|
||||||
|
Authorization: Bearer {{admin_token}}
|
||||||
|
|
||||||
|
HTTP 404
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$.error" == "job not registered"
|
||||||
|
jsonpath "$.name" == "no_such_job"
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
# Step 6 — Non-admin caller is denied by the `/api/admin/*`
|
||||||
|
# middleware layer. The handler itself has no bespoke
|
||||||
|
# role check — reaching it at all means the caller is
|
||||||
|
# admin (same shape as `dedup_admin_gate.hurl` pins).
|
||||||
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
GET {{base_url}}/api/admin/jobs
|
||||||
|
Authorization: Bearer {{bob_token}}
|
||||||
|
|
||||||
|
HTTP 403
|
||||||
|
|
||||||
|
|
||||||
|
POST {{base_url}}/api/admin/jobs/trash_cleanup/trigger
|
||||||
|
Authorization: Bearer {{bob_token}}
|
||||||
|
|
||||||
|
HTTP 403
|
||||||
@@ -166,6 +166,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
|||||||
"$API_DIR/batch_folder_copy.hurl" \
|
"$API_DIR/batch_folder_copy.hurl" \
|
||||||
"$API_DIR/dedup_blob_cleanup.hurl" \
|
"$API_DIR/dedup_blob_cleanup.hurl" \
|
||||||
"$API_DIR/dedup_admin_gate.hurl" \
|
"$API_DIR/dedup_admin_gate.hurl" \
|
||||||
|
"$API_DIR/admin_jobs.hurl" \
|
||||||
"$API_DIR/default_caldav_carddav.hurl" \
|
"$API_DIR/default_caldav_carddav.hurl" \
|
||||||
"$API_DIR/dav_error_mapping.hurl" \
|
"$API_DIR/dav_error_mapping.hurl" \
|
||||||
"$API_DIR/carddav_vcard_properties.hurl" \
|
"$API_DIR/carddav_vcard_properties.hurl" \
|
||||||
|
|||||||
Reference in New Issue
Block a user