|
| 1 | +# E2E tests for `clientapi_pve` |
| 2 | + |
| 3 | +Live-server pytest suite. Runs against a real Proxmox VE instance — by default the |
| 4 | +`ghcr.io/client-api/proxmox-docker/pve-test` container, either spun up locally |
| 5 | +via `docker compose up -d` or in CI via |
| 6 | +[`client-api/proxmox-docker-action@v1`](https://github.com/client-api/proxmox-docker-action). |
| 7 | + |
| 8 | +## Quick start (local) |
| 9 | + |
| 10 | +```bash |
| 11 | +docker compose up -d |
| 12 | +sleep 20 # wait for healthcheck |
| 13 | + |
| 14 | +export PROXMOX_URL=https://localhost:8006 |
| 15 | +export PROXMOX_USER=root@pam |
| 16 | +export PROXMOX_PASSWORD=proxmox123 |
| 17 | +# Read the joined token header from the container: |
| 18 | +export PROXMOX_TOKEN_HEADER_VALUE="$(docker exec pve-test cat /run/credentials.json | jq -r .token_header_value)" |
| 19 | +export PROXMOX_INSECURE=1 |
| 20 | +export PROXMOX_KVM_AVAILABLE=$(test -e /dev/kvm && echo true || echo false) |
| 21 | +export PROXMOX_CGROUPV2_AVAILABLE=$(test -d /sys/fs/cgroup/cgroup.controllers && echo true || echo false) |
| 22 | + |
| 23 | +pip install -e .[test] |
| 24 | +pytest e2e/ -v |
| 25 | +``` |
| 26 | + |
| 27 | +## Environment contract |
| 28 | + |
| 29 | +| Var | Source | Notes | |
| 30 | +|---|---|---| |
| 31 | +| `PROXMOX_URL` | `proxmox-docker-action` | e.g. `https://localhost:8006` (no `/api2/json`) | |
| 32 | +| `PROXMOX_USER` | action | typically `root@pam` | |
| 33 | +| `PROXMOX_PASSWORD` | action | password ticket auth (SC-10..11, SC-14) | |
| 34 | +| `PROXMOX_TOKEN_HEADER_VALUE` | action | whole `PVEAPIToken=root@pam!test=<uuid>` string | |
| 35 | +| `PROXMOX_TOKEN_VALUE` | action | UUID half only (rarely needed) | |
| 36 | +| `PROXMOX_INSECURE` | local | set to `1` to skip TLS verification (self-signed dev cert) | |
| 37 | +| `PROXMOX_KVM_AVAILABLE` | action output | `true`/`false`; gates SC-60, SC-62 | |
| 38 | +| `PROXMOX_CGROUPV2_AVAILABLE` | action output | `true`/`false`; gates SC-61 | |
| 39 | +| `PROXMOX_NO_NETWORK` | manual | `1` to skip network-egress tests (SC-35, SC-62) | |
| 40 | + |
| 41 | +> Never reconstruct `PROXMOX_TOKEN_HEADER_VALUE` by hand — the Perl family |
| 42 | +> (PVE, PMG) joins with `=`, the Rust family (PBS, PDM) with `:`. The |
| 43 | +> container pre-joins it correctly. |
| 44 | +
|
| 45 | +## Capability gates |
| 46 | + |
| 47 | +Tests that need kernel features import the marker symbol, never an inline env check: |
| 48 | + |
| 49 | +```python |
| 50 | +from e2e.conftest import requires_kvm |
| 51 | + |
| 52 | +@requires_kvm |
| 53 | +def test_vm_lifecycle(pve, node): |
| 54 | + ... |
| 55 | +``` |
| 56 | + |
| 57 | +When the gate is closed the test is **skipped**, not failed. |
| 58 | + |
| 59 | +## Fixture convention |
| 60 | + |
| 61 | +All entities created during the suite are named `e2e-…` (users, storages, |
| 62 | +ACLs) and live in the VM-ID range `101..199`. `cleanup_e2e()` runs at session |
| 63 | +start and end. VM 100 (`tiny-test`) and CT 200 (`tiny-ct`) are pre-seeded by |
| 64 | +the container and are never touched. |
| 65 | + |
| 66 | +## Scenario index (SC-01 … SC-62) |
| 67 | + |
| 68 | +| File | Scenarios | |
| 69 | +|---|---| |
| 70 | +| `test_version.py` | SC-01 | |
| 71 | +| `test_auth.py` | SC-10 … SC-14 | |
| 72 | +| `test_authz.py` | SC-20 … SC-22 | |
| 73 | +| `test_crud.py` | SC-30 … SC-34 | |
| 74 | +| `test_iso_upload.py` | SC-35 | |
| 75 | +| `test_errors.py` | SC-40 … SC-42 | |
| 76 | +| `test_types.py` | SC-50 … SC-52 | |
| 77 | +| `test_vm_lifecycle.py` | SC-60 (kvm-gated) | |
| 78 | +| `test_ct_lifecycle.py` | SC-61 (cgroupv2-gated) | |
| 79 | +| `test_vm_cdrom.py` | SC-62 (kvm+network-gated) | |
| 80 | + |
| 81 | +## Known SDK gaps surfaced by this suite |
| 82 | + |
| 83 | +These are real generator gaps the live tests uncovered; each has a focused |
| 84 | +workaround in `e2e/helpers/` so the rest of the suite stays clean, and the |
| 85 | +SC-NN that exercises it stays in the suite so it will auto-promote when the |
| 86 | +upstream template is fixed. |
| 87 | + |
| 88 | +1. **Ticket cookie format.** `Configuration.auth_settings` emits |
| 89 | + `Cookie: <ticket>` for `PVEAuthCookie`. PVE expects |
| 90 | + `Cookie: PVEAuthCookie=<ticket>`. Workaround in |
| 91 | + `e2e/helpers/clients.py::ticket_client` pre-joins the prefix. |
| 92 | +2. **`NodesStorageApi.upload` carries no file body.** The OpenAPI spec models |
| 93 | + the upload field as `tmpfilename` (a server-side path reference), so the |
| 94 | + generated SDK can only send metadata. Workaround: raw multipart POST in |
| 95 | + `e2e/helpers/upload.py::upload_iso`. |
| 96 | +3. **`NodesStorageDiridxResponse.data` types inner items as |
| 97 | + `AccessGetAccessResponseDataInner`.** The generator picked the wrong inner |
| 98 | + model, so `volid` deserializes as empty. Workaround: |
| 99 | + `e2e/helpers/upload.py::list_storage_content` parses the raw JSON. |
| 100 | +4. **`QemuCreateVmRequest.to_dict()` references undefined indexed-family |
| 101 | + fields** (`ide0`, `net0`, …). The model exposes collapsed `ides`/`nets` |
| 102 | + maps but the serializer reaches for individual numbered properties. |
| 103 | + SC-62 is marked `xfail` until the template is fixed. |
| 104 | +5. **POST endpoints with optional request models drop the body entirely.** |
| 105 | + `vm_start` / `vm_stop` (etc.) send no body when no request model is |
| 106 | + supplied; PVE returns 500 "malformed JSON". Tests always pass an empty |
| 107 | + request model (`QemuVmStartRequest()`) instead of relying on the default. |
| 108 | + |
| 109 | +## Downstream cells |
| 110 | + |
| 111 | +`pbs-py`, `pmg-py`, `pdm-py` follow the same shape — copy `e2e/`, the |
| 112 | +pyproject test group, `docker-compose.yml`, and `.github/workflows/e2e.yml`, |
| 113 | +then swap product-specific bits (image tag, port, token separator). PMG skips |
| 114 | +SC-12/13 (no API tokens). PDM uses `/api2/extjs` in raw HTTP paths. |
0 commit comments