content(guide): reshape fleet Testing tabs — slim frontend-only QA checklist
Per Johannes: Testing tab is a tester's what-to-test -> pass/fail checklist (UI only, bcos.dev sim data C1/C2/C3 + test logins), not an engineering API spec. Dropped curl/endpoints/backend. New pattern for the fan-out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,96 +6,28 @@ order: 35
|
||||
audience: internal
|
||||
---
|
||||
|
||||
> How to test the Fleet **Artifacts** module (owner: `hiveops-fleet`, port 8097, DB `hiveiq_fleet`, table `fleet_artifacts`). All examples target bcos.dev. External base for this module is `https://api.bcos.dev/fleet`; the frontend `fleetAPI` prefixes `/api/fleet`, so full paths are `https://api.bcos.dev/fleet/api/fleet/...`.
|
||||
Frontend test checklist for **Fleet → Artifacts** (`fleet.bcos.dev`). Click through in the UI and mark each row Pass or Fail.
|
||||
|
||||
## Test accounts
|
||||
## Set up
|
||||
Log in at **fleet.bcos.dev** — test accounts (all password `Passw0rd-d3v!`):
|
||||
|
||||
All bcos.dev test users share password `Passw0rd-d3v!`.
|
||||
| Login | Role | Can |
|
||||
|-------|------|-----|
|
||||
| `bcos_a@bcos.dev` | Admin | Import / enable / delete, all institutions |
|
||||
| `msp_a@msp1.bcos.dev` | MSP admin | Import / enable / delete, C1 + C2 |
|
||||
| `customer_a@customer1.bcos.dev` | Customer | View only (C1) |
|
||||
|
||||
| Account | Role | Why it matters here |
|
||||
|---------|------|---------------------|
|
||||
| `bcos_a@bcos.dev` | `BCOS_ADMIN` | Sees all institutions. Can import (CDN + multipart), toggle, delete, and browse the CDN manifest. Use for the happy-path mutations. |
|
||||
| `msp_a@msp1.bcos.dev` | `MSP_ADMIN` | Scoped to C1BD + C2BD. Can also import/toggle/delete + browse manifest. Use to confirm scoping and that mutate roles work below BCOS. |
|
||||
| `customer_a@customer1.bcos.dev` | `CUSTOMER` | Scoped to C1BD. Read-only: **can list/view** artifacts but must **not** import, delete, toggle, or browse the CDN manifest. Use for negative role checks. |
|
||||
## What to test
|
||||
|
||||
Only these three roles are needed. The mutate set is `ADMIN,MSP_ADMIN,BCOS_ADMIN`; the read set is `USER,CUSTOMER,ADMIN,MSP_ADMIN,BCOS_ADMIN`; the CDN-manifest browse is the narrower `MSP_ADMIN,BCOS_ADMIN`.
|
||||
| # | Do this | ✅ Pass if |
|
||||
|---|---------|-----------|
|
||||
| 1 | As `msp_a`, open **Fleet → Artifacts** | The artifacts list loads |
|
||||
| 2 | Click **Import** and choose **Agent** | The CDN options load and you can pick a version to import |
|
||||
| 3 | Import an agent version | It appears in the list, marked **Enabled** |
|
||||
| 4 | Repeat Import for **Hotfix** and **Software** | Each shows its available CDN versions and imports without error |
|
||||
| 5 | Toggle an artifact **Disabled**, then **Enabled** again | The status flips and stays after a page refresh |
|
||||
| 6 | **Delete** an artifact that isn't in use | It's removed from the list |
|
||||
| 7 | Log in as `customer_a`, open **Artifacts** | You can **view** the list but there are **no Import / Enable / Delete** controls (read-only) |
|
||||
| 8 | As `customer_a` vs `msp_a`, compare the lists | `customer_a` sees **only C1**; `msp_a` sees **C1 + C2**, never C3 |
|
||||
|
||||
## Manual test flow
|
||||
|
||||
Open the Fleet app → **Artifacts** tab (`FleetArtifacts.svelte`). Log in as `bcos_a@bcos.dev` unless a step says otherwise.
|
||||
|
||||
**Happy path — CDN import (preferred):**
|
||||
1. Click **Import Agent**. → The panel calls `GET /api/fleet/cdn-manifest/agent` and lists packages from `cdn.bcos.cloud/downloads/agent/latest.json` (patch + standard, Windows + Linux). Expected: rows appear, no "Failed to load CDN manifest" error.
|
||||
2. Import a **patch** package. → A new row appears with `artifactType` `UPDATE_AGENT` (UI label **Agent Patch**), **Enabled** = on, and a **SHA-256** column that is blank/`-`. That blank hash is EXPECTED for CDN imports — the file is HEAD-probed, never downloaded to fleet.
|
||||
3. Confirm the row shows a **Global** or **Institution** badge matching whether you set an institution key (blank = Global / all institutions).
|
||||
4. Click **Toggle Enabled** on the row. → It flips to disabled, then back on. (`POST /api/fleet/artifacts/{id}/toggle-enabled`.)
|
||||
5. **Delete** the test row while no task references it. → Row disappears (`DELETE /api/fleet/artifacts/{id}` returns 200/204).
|
||||
|
||||
**Edge case — delete blocked by an active task (409):**
|
||||
1. Import an artifact, then create a fleet task that references it and leave it `PENDING/QUEUED/RUNNING`.
|
||||
2. Try to delete the artifact. → Expected **409 Conflict** ("delete blocked while a task references it"). This is intentional. Cancel/drain the task, then delete succeeds.
|
||||
|
||||
**Edge case — unsupported manifest type:** there is no Module manifest. `GET /api/fleet/cdn-manifest/modules` returns **400 Unknown manifest type** — only `agent`, `hotfix`, `software` resolve. Don't expect a "Import Module" from CDN to work.
|
||||
|
||||
## API smoke test
|
||||
|
||||
```bash
|
||||
# 1. Login → capture JWT
|
||||
TOKEN=$(curl -s -X POST https://api.bcos.dev/auth/api/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"bcos_a@bcos.dev","password":"Passw0rd-d3v!"}' \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
|
||||
|
||||
BASE=https://api.bcos.dev/fleet/api/fleet
|
||||
|
||||
# 2. List artifacts (read) → 200, JSON array
|
||||
curl -s -o /dev/null -w '%{http_code}\n' "$BASE/artifacts" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 3. Filter by exact enum value (NOT the UI label) → 200
|
||||
curl -s "$BASE/artifacts?type=UPDATE_AGENT" \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool | head
|
||||
|
||||
# 4. Browse the CDN agent manifest (MSP_ADMIN/BCOS_ADMIN only) → 200
|
||||
curl -s -o /dev/null -w '%{http_code}\n' "$BASE/cdn-manifest/agent" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 5. Unsupported manifest type → 400 Unknown manifest type
|
||||
curl -s -o /dev/null -w '%{http_code}\n' "$BASE/cdn-manifest/modules" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Mutation (WRITES a row — run only against dev, then delete it):**
|
||||
```bash
|
||||
# import-cdn: JSON body. artifactType must be the enum value, not the UI label.
|
||||
curl -s "$BASE/artifacts/import-cdn" \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"cdnUrl":"https://cdn.bcos.cloud/downloads/agent/latest.json",
|
||||
"name":"smoke-test-agent","version":"0.0.0-test",
|
||||
"artifactType":"UPDATE_AGENT","institutionKey":"C1BD"}'
|
||||
# → 200 + new artifact JSON (cdn_url set, no sha256_hash). Delete it afterward:
|
||||
# curl -s -X DELETE "$BASE/artifacts/<id>" -H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Expected shapes: list/get return artifact objects with `artifactType`, `enabled`, `institutionKey` (null = Global), `cdnUrl`, and `sha256Hash` (null for CDN imports, set for multipart uploads).
|
||||
|
||||
## Verify roles & scoping
|
||||
|
||||
Get a token for each role (swap the email/password in the login snippet), then against `$BASE`:
|
||||
|
||||
- **CUSTOMER read allowed:** `GET /artifacts` as `customer_a@customer1.bcos.dev` → **200**.
|
||||
- **CUSTOMER mutate denied:** `POST /artifacts/import-cdn` and `DELETE /artifacts/{id}` as `customer_a` → **403** (mutate set is `ADMIN,MSP_ADMIN,BCOS_ADMIN`).
|
||||
- **CUSTOMER manifest denied:** `GET /cdn-manifest/agent` as `customer_a` → **403** (browse is `MSP_ADMIN,BCOS_ADMIN` only). This is the narrowest gate — verify a `CUSTOMER` and a plain `USER`/`ADMIN`-below check both fail it.
|
||||
- **MSP_ADMIN allowed:** as `msp_a@msp1.bcos.dev`, `import-cdn`, `toggle-enabled`, `delete`, and `cdn-manifest/agent` all succeed.
|
||||
- **Institution scoping:** import an artifact with `institutionKey: "C1BD"` and one with a blank key (Global). Confirm `customer_a` (C1BD) sees the C1BD-scoped one and the Global one, but not a C2BD/C3BD-scoped artifact. An artifact with a blank `institutionKey` is Global (all institutions); a set key scopes it. `institution_key` is capped at 10 chars.
|
||||
|
||||
## Watch for (regressions)
|
||||
|
||||
- **Blank SHA-256 on CDN imports flagged as a bug** — it is expected. Only multipart uploads (`POST /artifacts`) compute `sha256_hash` + `storage_path`. Don't "fix" a CDN row's missing hash.
|
||||
- **`410 GONE` on task download** — pointing an agent at fleet's chunk endpoint (`/tasks/{id}/download`) for a CDN-only artifact. CDN artifacts must download from `cdnUrl` directly; the agent must support `artifactCdnUrl` in `tasks/next`.
|
||||
- **`download_offset` stuck at 0 read as a stall** — for CDN artifacts the download bypasses fleet, so offset 0 is NORMAL.
|
||||
- **UI label sent as `artifactType`** — sending `"Agent Patch"` instead of `UPDATE_AGENT` (or `modules` to the manifest endpoint) must fail. Enum values are `UPDATE_AGENT · UPDATE_HOTFIX · UPDATE_SOFTWARE · UPDATE_MODULE · SCRIPT`.
|
||||
- **Manifest panel "Failed to load CDN manifest"** — usually `CDN_TOKEN` missing/stale on the fleet container (sent as `X-CDN-Token`). Curl the `latest.json` with that header from the fleet host to confirm.
|
||||
- **Delete not returning 409 while a `PENDING/QUEUED/RUNNING` task references the artifact** — that guard must hold; a successful delete under an active task is a regression.
|
||||
- **Multipart uploads over 100 MB accepted** — the cap is 100 MB; oversized files must be rejected and routed via CDN import.
|
||||
- **Artifact written outside `hiveiq_fleet.fleet_artifacts`** — fleet is the sole owner; no artifact endpoint should appear on incident/devices/mgmt.
|
||||
**Report a fail with:** the row #, which login you used, and what you actually saw.
|
||||
|
||||
@@ -6,100 +6,32 @@ order: 35
|
||||
audience: internal
|
||||
---
|
||||
|
||||
How to test the Patch Windows module on **bcos.dev**. Backend: `hiveops-fleet` (port 8097, DB `hiveiq_fleet`). Frontend: `FleetManagement/PatchWindows.svelte` at `fleet.bcos.dev` → Fleet Management → **Patch Windows** tab.
|
||||
Frontend test checklist for **Fleet → Patch Windows** (`fleet.bcos.dev`). Click through in the UI and mark each row Pass or Fail.
|
||||
|
||||
All test users share password `Passw0rd-d3v!`.
|
||||
## Set up
|
||||
Log in at **fleet.bcos.dev** — test accounts (all password `Passw0rd-d3v!`):
|
||||
|
||||
## Test accounts
|
||||
| Login | Role | Can |
|
||||
|-------|------|-----|
|
||||
| `bcos_a@bcos.dev` | Admin | Create / edit / delete, all institutions |
|
||||
| `msp_a@msp1.bcos.dev` | MSP admin | Create / edit / delete, C1 + C2 |
|
||||
| `customer_a@customer1.bcos.dev` | Customer | View only (C1) |
|
||||
|
||||
Which accounts matter for this module and why:
|
||||
Test devices are the simulators: **C1-ATM-001…** (C2-/C3- for the other institutions).
|
||||
|
||||
| Account | Role | Use it to test |
|
||||
|---------|------|----------------|
|
||||
| `bcos_a@bcos.dev` | `BCOS_ADMIN` | Unrestricted (scope = null). Create/edit/delete any window, assign ATMs, verify cross-institution visibility. |
|
||||
| `msp_a@msp1.bcos.dev` | `MSP_ADMIN` | Scoped to **C1BD + C2BD**. Can create/edit within scope; must get **403** on windows for `C3BD`. |
|
||||
| `customer_a@customer1.bcos.dev` | `CUSTOMER` | Scoped to **C1BD**, read-only. Can list/view C1BD windows; must **403** on any create/update/delete and on cross-institution reads. |
|
||||
## What to test
|
||||
|
||||
Institutions: `C1BD`, `C2BD`, `C3BD`. Live simulator devices: `C1-ATM-001…`, `C1-TCR-001…`, `C1-SRV-001…` (use a `C1-ATM-*` id for assignment tests).
|
||||
| # | Do this | ✅ Pass if |
|
||||
|---|---------|-----------|
|
||||
| 1 | As `msp_a`, open **Fleet → Patch Windows** | The patch windows list loads |
|
||||
| 2 | Click **New Window**, give it a name, pick **days** + a **start time**, save | The window appears in the list with the days/time you set |
|
||||
| 3 | Open the window and **assign** `C1-ATM-001` (and a couple more C1 devices) | The assigned device count updates |
|
||||
| 4 | **Edit** the window (change the time), save | The change persists after a refresh |
|
||||
| 5 | Try to create a **second** window on a device already assigned to one | It's blocked / warns — a device belongs to one window at a time |
|
||||
| 6 | **Delete** a test window | It's removed from the list |
|
||||
| 7 | Log in as `customer_a`, open **Patch Windows** | You can **view** but there is **no New / Edit / Delete** (read-only) |
|
||||
| 8 | As `customer_a` vs `msp_a`, compare the lists | `customer_a` sees **only C1**; `msp_a` sees **C1 + C2**, never C3 |
|
||||
|
||||
## Manual test flow
|
||||
> ⚠️ **Known issue (fleet #89):** weekday windows currently don't schedule (a day-code bug). Until fixed, don't fail the suite on "the task didn't fire at the window time" — that's the tracked bug, not a new one.
|
||||
|
||||
Log in to `fleet.bcos.dev` as `bcos_a@bcos.dev`, open Fleet Management → **Patch Windows**.
|
||||
|
||||
### Happy path — create, assign, schedule
|
||||
|
||||
1. Click **+ New Patch Window**. Name it `qa-weekly`, institution `C1BD`, pick a day, set start/end time and timezone, save.
|
||||
- **Expected:** row appears in the table; toolbar count increments; Schedule and Timezone columns render.
|
||||
2. Click the **terminals** link (ATMs column) on the `qa-weekly` row.
|
||||
- **Expected:** ATM assignment panel opens listing currently-assigned devices (empty at first).
|
||||
3. Assign a `C1-ATM-*` device to the window (via the assignment action / Device Management per the Customer tab), then reopen the panel.
|
||||
- **Expected:** the device is listed. DB check: `SELECT * FROM patch_window_devices WHERE atm_id='C1-ATM-001';` returns one row.
|
||||
4. Create a `HOTFIX_INSTALL` fleet task for that ATM with **no explicit `scheduledAt`**.
|
||||
- **Expected:** task lands in the **Scheduled** tab as `PENDING_APPROVAL` with `scheduled_at` = the window's next start slot (stored UTC), and the Fleet Tasks row shows `Window: qa-weekly · Scheduled …`.
|
||||
|
||||
### Edge case — the `days_of_week` short-code gotcha (#1 failure mode)
|
||||
|
||||
5. Create a window through the **UI** and assign an ATM, then create a `HOTFIX_INSTALL` for it.
|
||||
- **What to watch:** the UI stores short codes (`MON,TUE,…`) but `PatchWindowService.nextOccurrence` calls `DayOfWeek.valueOf(...)`, which needs full names (`MONDAY`). Short codes throw, `FleetTaskService` catches + logs `Could not compute next patch window for device …`, and leaves `scheduled_at = null`.
|
||||
- **Expected (current behavior):** the install becomes **immediately dispatchable after approval** — no window hold. Confirm with `SELECT days_of_week FROM patch_windows WHERE name='...';` (short codes) and a null `scheduled_at`. This is the canonical "window ignored" regression — verify it every release.
|
||||
|
||||
### Edge case — approval gate
|
||||
|
||||
6. With a correctly scheduled `HOTFIX_INSTALL` (`scheduled_at` in the future), do nothing.
|
||||
- **Expected:** task stays `PENDING_APPROVAL`; it will **not** dispatch until an approver (`FLEET_APPROVER` claim or `BCOS_ADMIN`) approves — even after the window opens.
|
||||
|
||||
## API smoke test
|
||||
|
||||
External base on bcos.dev is `https://api.bcos.dev`; the fleet gateway prefix is `/fleet`, so paths below map to the backend controller `/api/fleet/patch-windows`.
|
||||
|
||||
```bash
|
||||
# 1. Login → $TOKEN
|
||||
TOKEN=$(curl -s -X POST https://api.bcos.dev/auth/api/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"bcos_a@bcos.dev","password":"Passw0rd-d3v!"}' \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
|
||||
|
||||
# 2. GET list — 200, JSON array of windows (each has id, name, institutionKey, daysOfWeek[], timezone, deviceCount)
|
||||
curl -s https://api.bcos.dev/fleet/patch-windows \
|
||||
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
|
||||
|
||||
# 3. GET one window by id — 200 (use an id from step 2); 404 if missing
|
||||
curl -s https://api.bcos.dev/fleet/patch-windows/1 \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 4. GET devices attached to a window — 200, array (empty [] if none)
|
||||
curl -s https://api.bcos.dev/fleet/patch-windows/1/atms \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 5. (MUTATION — writes a row) POST create a window.
|
||||
# Body = CreatePatchWindowRequest: institutionKey, name, daysOfWeek[], weekOfMonth?, startTime, endTime, timezone.
|
||||
# NOTE: send FULL DayOfWeek names (MONDAY) so nextOccurrence can schedule — short codes (MON) silently break scheduling.
|
||||
curl -s -X POST https://api.bcos.dev/fleet/patch-windows \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"institutionKey":"C1BD","name":"qa-smoke","daysOfWeek":["MONDAY"],"startTime":"02:00","endTime":"04:00","timezone":"America/New_York"}'
|
||||
# Expected: 200/201 with the created window. Re-running with the same (institutionKey,name) → 409 (unique per institution).
|
||||
```
|
||||
|
||||
> The `?institution=` param the UI sends is **ignored** by the controller (it reads `institutionKey`). A bare list returns everything the caller's scope allows, not a filtered set. To actually filter, pass `?institutionKey=C1BD`.
|
||||
|
||||
## Verify roles & scoping
|
||||
|
||||
`create` does **not** scope-check (trusts body `institutionKey`); every other read/mutate runs `assertInScope`. Run each check with the account's `$TOKEN`.
|
||||
|
||||
- **CUSTOMER read-only:** as `customer_a@customer1.bcos.dev`, `GET /fleet/patch-windows` → 200 (C1BD windows only). `POST/PUT/DELETE /fleet/patch-windows/...` → **403**.
|
||||
- **MSP_ADMIN in scope:** as `msp_a@msp1.bcos.dev`, list/create/edit a **C1BD** or **C2BD** window → allowed. `GET /fleet/patch-windows/{id}` or `PUT` on a **C3BD** window → **403** (out of scope).
|
||||
- **BCOS_ADMIN unrestricted:** as `bcos_a@bcos.dev`, sees and mutates windows across C1BD/C2BD/C3BD (scope resolves to null).
|
||||
- **Cross-institution read block:** as `customer_a`, GET a window whose `institution_key` is `C2BD`/`C3BD` → **403**, not 200.
|
||||
- **Guide/role-gated content:** the ⓘ **Guide** panel (`GuidePanel pageKey="fleet.patch-windows"`) should show the Customer tab to customers; internal tabs (Internal/Claude/Testing, `audience: internal`) must **not** surface to a `CUSTOMER` login.
|
||||
|
||||
## Watch for (regressions)
|
||||
|
||||
Tied to the module's known gotchas:
|
||||
|
||||
- **Window ignored / install runs immediately** — the `days_of_week` short-code vs `DayOfWeek.valueOf` bug. If a UI-created window stores `MON,TUE` instead of `MONDAY,TUESDAY`, `scheduled_at` stays null and the hold is lost. Grep logs for `Could not compute next patch window for device`. #1 thing to re-verify.
|
||||
- **ATM count column blank/`undefined terminals`** — DTO returns `deviceCount` but the frontend reads `atmCount` (field drift). The DB count via `countByPatchWindowId` is authoritative: `SELECT count(*) FROM patch_window_devices WHERE patch_window_id = ?;`.
|
||||
- **Institution filter does nothing** — UI sends `?institution=`, controller wants `?institutionKey=`. Filtering "returns everything" is expected until aligned.
|
||||
- **No end-time cutoff** — dispatch (`GET /api/internal/fleet/tasks/next`) gates only on `scheduledAt <= now`. A task eligible at the window **start** stays dispatchable indefinitely; there is no hard stop at window end. Don't file "task ran after the window closed" as a new bug — it's by design today.
|
||||
- **ATM in two windows** — schema only enforces `UNIQUE(patch_window_id, atm_id)`, not one-window-per-ATM; the scheduler uses `findByAtmId(...).findFirst()` (arbitrary pick). Assigning the same ATM to two windows should not error but is unsupported.
|
||||
- **Approval swallows the schedule** — `HOTFIX_INSTALL`/`SOFTWARE_INSTALL` are `PENDING_APPROVAL`; only these `_INSTALL` kinds are window-scheduled. Immediate `HOTFIX`/`SOFTWARE` (download) tasks are never window-gated — don't expect them to schedule.
|
||||
- **Duplicate name** — create/update with an existing `(institution_key, name)` must return **409**.
|
||||
**Report a fail with:** the row #, which login you used, and what you actually saw.
|
||||
|
||||
@@ -6,109 +6,32 @@ order: 35
|
||||
audience: internal
|
||||
---
|
||||
|
||||
How to test Fleet Task Workflow (owner service **hiveops-fleet**) on the bcos.dev environment. Everything below maps to real endpoints (`FleetApiController` / `InternalFleetController`) and the `FleetManagement/FleetTasks.svelte` UI. Test env base URL: `https://api.bcos.dev/fleet`, so full user endpoint = `https://api.bcos.dev/fleet/api/fleet/...`. Frontend: `fleet.bcos.dev`.
|
||||
Frontend test checklist for **Fleet Tasks** (`fleet.bcos.dev`). Click through in the UI and mark each row Pass or Fail.
|
||||
|
||||
## Test accounts
|
||||
## Set up
|
||||
Log in at **fleet.bcos.dev** — test accounts (all password `Passw0rd-d3v!`):
|
||||
|
||||
All test users share password `Passw0rd-d3v!`. For this module you mainly need three:
|
||||
| Login | Role | Sees |
|
||||
|-------|------|------|
|
||||
| `bcos_a@bcos.dev` | Admin | All institutions · can approve |
|
||||
| `msp_a@msp1.bcos.dev` | MSP admin | C1 + C2 only |
|
||||
| `customer_a@customer1.bcos.dev` | Customer | C1 only · read-only |
|
||||
|
||||
| Account | Role | Why it matters here |
|
||||
|---------|------|--------------------|
|
||||
| `bcos_a@bcos.dev` | BCOS_ADMIN | Sees all institutions; **can always approve** (BCOS_ADMIN bypasses the `fleetTaskApprover` claim). Use to create + approve HOTFIX/SOFTWARE tasks and to confirm cross-institution visibility. |
|
||||
| `msp_a@msp1.bcos.dev` | MSP_ADMIN (scoped C1BD+C2BD) | Can **create/cancel/retry** but **cannot approve** unless it also carries the `fleetTaskApprover` claim. Use to test institution scoping and the "MSP can create, not approve" gotcha. |
|
||||
| `customer_a@customer1.bcos.dev` | CUSTOMER (scoped C1BD) | Read-only for this module: can **view** tasks/history/stats/devices but any create/cancel/retry/approve must **403**. Use to verify scoping (should see only C1BD) and role gating. |
|
||||
Test devices are the simulators: **C1-ATM-001…**, **C1-TCR-001…** (and C2-/C3- for the other institutions).
|
||||
|
||||
Target devices are the live simulators per institution: `C1-ATM-001..`, `C1-TCR-001..`, `C1-SRV-001..` (and C2-/C3- for the other institutions). `atmIds` in the create request are `device_summary.id` values — get them from `GET /api/fleet/devices`, not the device name.
|
||||
## What to test
|
||||
|
||||
## Manual test flow
|
||||
| # | Do this | ✅ Pass if |
|
||||
|---|---------|-----------|
|
||||
| 1 | As `msp_a`, open **Fleet Tasks** | Page loads with tabs **Current / Completed / Failed / Approvals** |
|
||||
| 2 | **Create Task** → kind **Reboot** → pick `C1-ATM-001` → submit | New task shows in **Current** as **Pending** |
|
||||
| 3 | Watch it (device polls within ~1 min) | Status moves Pending → Queued → Running → **Completed**, then leaves Current and appears in **Completed** |
|
||||
| 4 | **Create Task** → kind **Hotfix** with an enabled artifact | Task shows as **Pending Approval** and appears in the **Approvals** tab |
|
||||
| 5 | Still as `msp_a`, try to approve it | Approve is **blocked** (no button / disabled / error) — MSP admins can't approve |
|
||||
| 6 | Log in as `bcos_a`, open **Approvals**, click **Approve** | Task moves to **Pending** and continues as normal |
|
||||
| 7 | Log in as `customer_a`, open **Fleet Tasks** | You can **view** tasks but there is **no Create Task button** (read-only) |
|
||||
| 8 | As `customer_a`, look at the device/task lists | You see **only C1** items — no C2 or C3 |
|
||||
| 9 | As `msp_a`, look at the lists | You see **C1 and C2** but **not C3** |
|
||||
| 10 | Create a task targeting a device shown as **Disconnected** | No task is created for it and the page shows a skip/none message — **no error/crash** |
|
||||
|
||||
Do these in the `fleet.bcos.dev` UI (Fleet Tasks → tabs **Current / Completed / Failed / Approvals**).
|
||||
|
||||
### Happy path — create a non-approval task (as `msp_a`)
|
||||
|
||||
1. Log in as `msp_a@msp1.bcos.dev`, open **Fleet Tasks**.
|
||||
2. Click **Create Task**. Pick a simple kind such as **REBOOT** or **CONFIG_UPDATE**, select one online C1 device (e.g. `C1-ATM-001`), submit.
|
||||
- **Expected:** task appears in the **Current** tab with status **PENDING** (non-approval kinds start PENDING).
|
||||
3. Wait for the device to poll (agent claims via `/tasks/next`).
|
||||
- **Expected:** status advances **PENDING → QUEUED → RUNNING → COMPLETED**. Once **COMPLETED** it is **archived immediately** — it disappears from Current and shows under **Completed** (history), not in the active list.
|
||||
4. Open the **Failed** tab and confirm nothing spurious landed there.
|
||||
|
||||
### Approval path — HOTFIX/SOFTWARE (needs `bcos_a`)
|
||||
|
||||
1. As `msp_a`, create a **HOTFIX** (or **SOFTWARE**) task against a C1 device using an **enabled** artifact.
|
||||
- **Expected:** task is created with status **PENDING_APPROVAL** (only `HOTFIX`, `HOTFIX_INSTALL`, `SOFTWARE`, `SOFTWARE_INSTALL` start here).
|
||||
2. Still as `msp_a`, open the **Approvals** tab.
|
||||
- **Expected:** if `msp_a` lacks the `fleetTaskApprover` claim, it can see the queue (MSP_ADMIN may list) but **Approve/Reject must fail** — the action returns 403 / is not offered.
|
||||
3. Log in as `bcos_a@bcos.dev`, open **Approvals**, add an optional note, click **Approve**.
|
||||
- **Expected:** task moves **PENDING_APPROVAL → PENDING**, then proceeds as the happy path once the device polls. **Reject** instead → **REJECTED**, archived immediately.
|
||||
|
||||
### Edge case — all targets skipped (silent skip / HTTP 207)
|
||||
|
||||
Grounded in the "create silently skips" gotcha.
|
||||
|
||||
1. As `msp_a`, create any task but target **only an offline device** (a device whose `lastHeartbeat` is null or > 15 min old) or a device outside C1BD/C2BD scope.
|
||||
- **Expected:** **no task appears** in Current. The POST returned **HTTP 207** with an **empty array** — it does *not* error loudly. Confirm via API (below) or by grepping the fleet backend log (Loki) for `skipping task creation` / `is offline` / `out of scope`.
|
||||
2. Second edge case: create a HOTFIX referencing a **disabled** artifact.
|
||||
- **Expected:** **HTTP 400** `Artifact '<name>' is disabled`. Enable it in the Artifacts view first, then retry.
|
||||
|
||||
## API smoke test
|
||||
|
||||
Get a token, then hit this module's real endpoints. Reads are safe; the mutation is flagged.
|
||||
|
||||
```bash
|
||||
# 1) Login → $TOKEN
|
||||
TOKEN=$(curl -s -X POST https://api.bcos.dev/auth/api/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"bcos_a@bcos.dev","password":"Passw0rd-d3v!"}' \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
|
||||
|
||||
BASE=https://api.bcos.dev/fleet
|
||||
|
||||
# 2) List tasks (paginated) — expect 200, page object of active tasks
|
||||
curl -s "$BASE/api/fleet/tasks/paginated?page=0&size=20" \
|
||||
-H "Authorization: Bearer $TOKEN" | head -c 400
|
||||
|
||||
# 3) Am I an approver? — expect 200 {"fleetTaskApprover": true|false}
|
||||
curl -s "$BASE/api/fleet/users/me" -H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 4) Target devices (mirror) — grab device_summary.id values for atmIds
|
||||
curl -s "$BASE/api/fleet/devices" -H "Authorization: Bearer $TOKEN" | head -c 400
|
||||
|
||||
# 5) Pending-approval queue — expect 200 array (empty if none)
|
||||
curl -s "$BASE/api/fleet/tasks/pending-approval" -H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 6) History (defaults to last 90 days) — expect 200
|
||||
curl -s "$BASE/api/fleet/tasks/history/recent" -H "Authorization: Bearer $TOKEN" | head -c 400
|
||||
```
|
||||
|
||||
Mutation (WRITES — creates a real task, only run against a disposable device):
|
||||
|
||||
```bash
|
||||
# POST /api/fleet/tasks — REBOOT one device. Replace <ID> with a real device_summary.id.
|
||||
# Expect 200/201 with the created task, OR HTTP 207 + [] if the target was skipped (offline/out-of-scope).
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$BASE/api/fleet/tasks" \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"atmIds":[<ID>],"taskKind":"REBOOT"}'
|
||||
```
|
||||
|
||||
Do **not** call `/api/internal/fleet/**` in smoke tests — those require the `X-Internal-Secret` header (agent-proxy only) and a user JWT will 403.
|
||||
|
||||
## Verify roles & scoping
|
||||
|
||||
- **CUSTOMER is read-only.** As `customer_a`, `GET /api/fleet/tasks/paginated` → **200**, but `POST /api/fleet/tasks` (and `/cancel`, `/retry`, `/approve`) → **403** (create/cancel/retry require `ADMIN`/`MSP_ADMIN`/`BCOS_ADMIN`).
|
||||
- **Institution scoping.** As `customer_a` (C1BD) you should see **only C1** devices/tasks. As `msp_a` (C1BD+C2BD) you should see C1 and C2 but **not C3**. As `bcos_a` you should see **all** institutions. Confirm via `GET /api/fleet/devices` and `GET /api/fleet/tasks` returning only in-scope institutions.
|
||||
- **Approver gating (the key one).** `GET /api/fleet/users/me` → check `fleetTaskApprover`. A plain `MSP_ADMIN` **without** the claim: `POST /api/fleet/tasks/{id}/approve` and `/reject` must **403**. `BCOS_ADMIN` (`bcos_a`) always succeeds. `fleetTaskApprover` is a Boolean JWT claim, **not** a `ROLE_`.
|
||||
- **Scope on create.** Have `msp_a` target a **C3** device id in `atmIds` → it is silently dropped (out of scope); if that was the only target the POST returns **207 + []**, not a task.
|
||||
- **Clear history is scoped.** `DELETE /api/fleet/tasks/history` only clears the caller's own institutions — as `msp_a` it must not touch C3 history.
|
||||
|
||||
## Watch for (regressions)
|
||||
|
||||
Tied to the known gotchas — these are the things most likely to break:
|
||||
|
||||
- **Silent-skip masking failures.** A create that targets offline (>15 min heartbeat) / out-of-scope / unknown devices returns **207 + empty array** instead of erroring. Regression = it starts 500-ing, or (worse) creates tasks against offline/out-of-scope devices. Always check the returned array length, not just the HTTP status.
|
||||
- **COMPLETED vs FAILED archival.** COMPLETED must be archived+deleted from `fleet_tasks` immediately (shows only in Completed/history). FAILED must **stay** in the active list (retryable) and must **not** be swept by the 5-min archiver. Regression = FAILED disappearing, or COMPLETED lingering in Current.
|
||||
- **Stale self-heal.** A QUEUED/RUNNING task with no update for >5 min must auto-reset to **PENDING** with `downloadOffset=0` on the next poll. Regression = tasks wedged in QUEUED/RUNNING forever.
|
||||
- **Approval enum coverage.** Only `HOTFIX`, `HOTFIX_INSTALL`, `SOFTWARE`, `SOFTWARE_INSTALL` start `PENDING_APPROVAL`; all other kinds start `PENDING`. Regression = a normal kind (REBOOT/CONFIG_UPDATE) getting stuck in PENDING_APPROVAL, or a hotfix skipping approval.
|
||||
- **Approve/reject 409.** Approving a task that is no longer `PENDING_APPROVAL` must **409** (not silently double-apply). Same for cancel on a terminal task and retry on a non-FAILED/CANCELLED task.
|
||||
- **Disabled-artifact guard.** Create with a disabled artifact must **400** `Artifact '<name>' is disabled` — not create a task that later fails on the device.
|
||||
- **Don't trust `GET /stats`.** `pendingTasks` only counts `AUTO_UPDATE`+`PENDING`; `completed`/`failed` are hardcoded `0`. Verify counts against the tabs/history, and don't file a "stats wrong" bug for this known limitation.
|
||||
**Report a fail with:** the row #, which login you used, and what you actually saw.
|
||||
|
||||
Reference in New Issue
Block a user