Files
Oxicloud/tests/api/recoverable_jobs.hurl
T
Edouard Vanbelle 8b0fb03b5c feat(recoverable jobs): clarify life cycle pause vs cancel
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
2026-08-02 17:38:44 +02:00

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