8b0fb03b5c
a job can be paused/resumed
a job as an exclusibity by it's name
if you want to run another job with same name:
either cancel the first one, or wait of it's terminaison
pause does not permit to run the other job, this can create
race conditions
195 lines
8.8 KiB
Plaintext
195 lines
8.8 KiB
Plaintext
# =============================================================
|
|
# OxiCloud — Recoverable-run admin surface
|
|
# =============================================================
|
|
# Pins the Part 2 (recoverable-run engine) admin endpoints:
|
|
# * POST /api/admin/jobs/{name}/trigger (RecoverableJobHandler
|
|
# path via RecoverableAdapter)
|
|
# * POST /api/admin/jobs/{name}/cancel
|
|
# * GET /api/admin/jobs/{name}/runs
|
|
# * GET /api/admin/jobs/{name}/runs/{id}
|
|
#
|
|
# Uses `drives_consistency` — the first recoverable tenant, on-demand
|
|
# only. Verifies:
|
|
# 1. Registered job appears in `GET /api/admin/jobs` with no
|
|
# interval (on-demand only).
|
|
# 2. Triggering creates a fresh row in `jobs.recoverable_runs`,
|
|
# handler completes, run terminates as Completed.
|
|
# 3. History endpoint returns the just-completed run.
|
|
# 4. Single-run detail endpoint returns the same row.
|
|
# 5. Cancel-on-idle is a no-op with `cancelled: false` (nothing
|
|
# running to cancel).
|
|
# 6. Unknown run id → 404 on the single-run endpoint.
|
|
# 7. Non-admin caller → 403 from the admin middleware on every
|
|
# recoverable endpoint (no bespoke role check in the handlers).
|
|
#
|
|
# Drift-finding assertions land alongside the `jobs.run_findings`
|
|
# migration — the current build LOGS findings to
|
|
# `oxicloud::consistency` without persisting them. Log-tail
|
|
# assertions from Hurl are fragile so we defer them.
|
|
# =============================================================
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Setup — admin login + rjobs_bob (non-admin) 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": "rjobs_bob",
|
|
"email": "rjobs_bob@example.com",
|
|
"password": "RjobsBobPassword1!"
|
|
}
|
|
|
|
HTTP 200
|
|
|
|
|
|
POST {{base_url}}/api/auth/login
|
|
Content-Type: application/json
|
|
{ "username": "rjobs_bob", "password": "RjobsBobPassword1!" }
|
|
|
|
HTTP 200
|
|
[Captures]
|
|
bob_token: jsonpath "$.access_token"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 1 — `drives_consistency` is registered on-demand only.
|
|
# Appears in the listing without an `interval_ms`.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/admin/jobs
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$[*].name" contains "drives_consistency"
|
|
# On-demand → no interval_ms (`skip_serializing_if = Option::is_none`).
|
|
jsonpath "$[?(@.name=='drives_consistency')].interval_ms" not exists
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 2 — Trigger the check. Handler dispatches through
|
|
# `RecoverableAdapter` → `run_or_resume`, which INSERTs
|
|
# a fresh `jobs.recoverable_runs` row, runs the scan,
|
|
# marks it Completed. Response envelope:
|
|
# { ok, outcome: { outcome: "ok",
|
|
# extra: { completed: true, run_id: "..." } } }
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/admin/jobs/drives_consistency/trigger
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$.ok" == true
|
|
jsonpath "$.outcome.outcome" == "ok"
|
|
jsonpath "$.outcome.extra.completed" == true
|
|
[Captures]
|
|
run_id: jsonpath "$.outcome.extra.run_id"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 3 — Run history returns at least the just-triggered
|
|
# run, newest first. Response is a JSON array of
|
|
# RunSummary; the top entry must be the run_id we
|
|
# captured above with status='Completed'.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/admin/jobs/drives_consistency/runs
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$" isCollection
|
|
jsonpath "$[0].id" == "{{run_id}}"
|
|
jsonpath "$[0].job_name" == "drives_consistency"
|
|
jsonpath "$[0].status" == "Completed"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 4 — Single-run detail. Returns the same row shape as
|
|
# the listing but for one id.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/admin/jobs/drives_consistency/runs/{{run_id}}
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$.id" == "{{run_id}}"
|
|
jsonpath "$.job_name" == "drives_consistency"
|
|
jsonpath "$.status" == "Completed"
|
|
# `stats` is always present (JSONB NOT NULL DEFAULT '{}'); its
|
|
# per-key shape is job-specific. `scanned_count` is bumped by the
|
|
# handler's `checkpoint()` call, but the drives handler only
|
|
# checkpoints when it processes a batch — a run that finds zero
|
|
# rows in the first batch (e.g. all fixture drives sit inside the
|
|
# 1h grace window on `storage.drives.created_at`) completes
|
|
# without ever calling checkpoint, so `scanned_count` may be
|
|
# absent. Pin `stats` existence; leave the counter unpinned.
|
|
jsonpath "$.stats" exists
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 5 — Cancel-on-idle is a no-op. `cancel` is TERMINAL — it
|
|
# acts on any non-terminal row (Running, CancelRequested,
|
|
# or Paused). No such row → 200 with `cancelled: false`.
|
|
# NOT a 404 (job name is registered, cancel just found
|
|
# nothing to abandon).
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/admin/jobs/drives_consistency/cancel
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 200
|
|
[Asserts]
|
|
jsonpath "$.cancelled" == false
|
|
jsonpath "$.reason" == "no non-terminal run for this job"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 6 — Unknown run id → 404. UUID shape is valid; the id
|
|
# just isn't in `jobs.recoverable_runs`.
|
|
# ─────────────────────────────────────────────────────────────
|
|
GET {{base_url}}/api/admin/jobs/drives_consistency/runs/00000000-0000-0000-0000-000000000000
|
|
Authorization: Bearer {{admin_token}}
|
|
|
|
HTTP 404
|
|
[Asserts]
|
|
jsonpath "$.error" == "run not found"
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Step 7 — Non-admin caller is denied on every endpoint by
|
|
# the `/api/admin/*` middleware layer. Handlers have
|
|
# no bespoke role check — reaching them at all means
|
|
# the caller is admin.
|
|
# ─────────────────────────────────────────────────────────────
|
|
POST {{base_url}}/api/admin/jobs/drives_consistency/trigger
|
|
Authorization: Bearer {{bob_token}}
|
|
|
|
HTTP 403
|
|
|
|
|
|
POST {{base_url}}/api/admin/jobs/drives_consistency/cancel
|
|
Authorization: Bearer {{bob_token}}
|
|
|
|
HTTP 403
|
|
|
|
|
|
GET {{base_url}}/api/admin/jobs/drives_consistency/runs
|
|
Authorization: Bearer {{bob_token}}
|
|
|
|
HTTP 403
|
|
|
|
|
|
GET {{base_url}}/api/admin/jobs/drives_consistency/runs/{{run_id}}
|
|
Authorization: Bearer {{bob_token}}
|
|
|
|
HTTP 403
|