Skip to content

Commit 771c1ae

Browse files
committed
test(e2e): add universal-core suite + CI
Mirrors pve-python e2e/ shape for the PMG cell. Token-related scenarios (SC-12/13/42) and ACL scenarios (SC-22/33) omitted because PMG has neither API tokens nor an ACL endpoint. Scenarios kept: SC-01 (version), SC-10/11/14 (ticket auth + CSRF), SC-30/31 (user CRUD), SC-41 (input validation), SC-50 (int64 uptime/time), SC-51 (nullable). PMG-specific: every `accessUsers.create_users` requires a `role` field (PmgRoleEnum) — tests use AUDIT for transient e2e users. Credentials helper drops the token_header_value field since PMG has no tokens. Local CI image is `pmg-test:latest` on port 8006. Workflow installs pytest deps explicitly because the regen drops `[project.optional-dependencies]` from pyproject.toml.
1 parent 2e3232d commit 771c1ae

17 files changed

Lines changed: 492 additions & 0 deletions

.github/workflows/e2e.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: e2e
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
pmg:
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 20
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.13'
19+
cache: pip
20+
21+
- name: Install package + test deps
22+
run: |
23+
pip install -e .
24+
pip install 'pytest>=8' 'pytest-timeout>=2.3' 'requests>=2.32'
25+
26+
- name: Authenticate to GHCR
27+
uses: docker/login-action@v3
28+
with:
29+
registry: ghcr.io
30+
username: ${{ github.actor }}
31+
password: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Start PMG test container
34+
id: proxmox
35+
uses: client-api/proxmox-docker-action@v1
36+
with:
37+
product: pmg
38+
tag: latest
39+
40+
- name: Run E2E tests
41+
run: pytest e2e/ -v --tb=short
42+
env:
43+
PROXMOX_INSECURE: '1'

.openapi-generator-ignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# OpenAPI Generator Ignore
2+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
3+
#
4+
# Use this file to prevent files from being overwritten by the generator.
5+
6+
# Hand-written E2E suite — never regenerate.
7+
e2e/
8+
e2e/**
9+
10+
# CI workflow we own; generator only manages ci.yml + publish.yml.
11+
.github/workflows/e2e.yml
12+
13+
# Local docker harness for the E2E suite.
14+
docker-compose.yml

docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
services:
2+
pmg-test:
3+
image: ghcr.io/client-api/proxmox-docker/pmg-test:latest
4+
container_name: pmg-test
5+
privileged: true
6+
tmpfs:
7+
- /tmp
8+
- /run
9+
- /run/lock
10+
ports:
11+
- "8006:8006"
12+
healthcheck:
13+
test: ["CMD-SHELL", "curl -sk -o /dev/null -w '%{http_code}' https://localhost:8006/api2/json/version | grep -qE '^(200|401)$'"]
14+
interval: 5s
15+
timeout: 5s
16+
retries: 24
17+
start_period: 10s

e2e/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# E2E tests for `clientapi_pmg`
2+
3+
Live-server pytest suite against a real Proxmox Mail Gateway instance.
4+
5+
## Quick start (local)
6+
7+
```bash
8+
docker compose up -d
9+
sleep 20
10+
11+
export PROXMOX_URL=https://localhost:8006
12+
export PROXMOX_USER=root@pam
13+
export PROXMOX_PASSWORD=proxmox123
14+
export PROXMOX_INSECURE=1
15+
16+
pip install -e .
17+
pip install 'pytest>=8' 'pytest-timeout>=2.3' requests
18+
pytest e2e/ -v
19+
```
20+
21+
PMG does not have API tokens — `PROXMOX_TOKEN_HEADER_VALUE` is unused.
22+
23+
## Scenario index
24+
25+
PMG-applicable subset (no API tokens → SC-12/13/42 omitted):
26+
27+
| File | Scenarios |
28+
|---|---|
29+
| `test_version.py` | SC-01 |
30+
| `test_auth.py` | SC-10 (ticket login), SC-11 (invalid pw), SC-14 (CSRF on writes) |
31+
| `test_crud.py` | SC-30, SC-31 (user CRUD) |
32+
| `test_errors.py` | SC-41 (input validation) |
33+
| `test_types.py` | SC-50 (int64 uptime/time), SC-51 (nullable) |
34+
35+
Storage CRUD, ISO upload, VM/CT lifecycle, and oneOf discriminator scenarios
36+
are PVE-specific and live in `pve-python/e2e/`.

e2e/__init__.py

Whitespace-only changes.

e2e/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Shared pytest fixtures for the PMG E2E suite.
2+
3+
PMG uses ticket auth only — no API tokens (SC-12/13/42 don't apply).
4+
"""
5+
from __future__ import annotations
6+
7+
from typing import Iterator
8+
9+
import pytest
10+
11+
from clientapi_pmg import Pmg
12+
from e2e.helpers.clients import issue_ticket
13+
from e2e.helpers.credentials import Credentials, MissingCredentialError
14+
from e2e.helpers.fixtures import cleanup_e2e
15+
16+
17+
@pytest.fixture(scope="session")
18+
def creds() -> Credentials:
19+
try:
20+
return Credentials.from_env()
21+
except MissingCredentialError as exc:
22+
pytest.skip(str(exc))
23+
24+
25+
@pytest.fixture(scope="session")
26+
def pmg(creds: Credentials) -> Pmg:
27+
"""PMG client authenticated via ticket (no API tokens on PMG)."""
28+
return issue_ticket(creds)
29+
30+
31+
@pytest.fixture(scope="session", autouse=True)
32+
def _session_cleanup(creds: Credentials, pmg: Pmg) -> Iterator[None]:
33+
cleanup_e2e(pmg)
34+
yield
35+
cleanup_e2e(pmg)

e2e/helpers/__init__.py

Whitespace-only changes.

e2e/helpers/capability_gate.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""Capability gates exposed by client-api/proxmox-docker-action."""
2+
from __future__ import annotations
3+
4+
import os
5+
6+
7+
def _truthy(name: str) -> bool:
8+
return os.environ.get(name, "").lower() in ("1", "true", "yes")
9+
10+
11+
def kvm_available() -> bool:
12+
return _truthy("PROXMOX_KVM_AVAILABLE")
13+
14+
15+
def cgroupv2_available() -> bool:
16+
return _truthy("PROXMOX_CGROUPV2_AVAILABLE")
17+
18+
19+
def network_available() -> bool:
20+
return os.environ.get("PROXMOX_NO_NETWORK", "") != "1"

e2e/helpers/clients.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Client factories for PMG ticket auth (PMG has no API tokens)."""
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING
5+
6+
from clientapi_pmg import Configuration, Pmg
7+
8+
if TYPE_CHECKING:
9+
from e2e.helpers.credentials import Credentials
10+
11+
12+
def ticket_client(
13+
creds: "Credentials",
14+
*,
15+
ticket: str,
16+
csrf: str | None = None,
17+
) -> Pmg:
18+
cfg = Configuration(host=f"{creds.url}/api2/json")
19+
cfg.verify_ssl = not creds.insecure
20+
cfg.api_key["PMGAuthCookie"] = ticket
21+
if csrf is not None:
22+
cfg.api_key["CSRFPreventionToken"] = csrf
23+
return Pmg(cfg)
24+
25+
26+
def issue_ticket(creds: "Credentials", *, password: str | None = None) -> Pmg:
27+
from clientapi_pmg.models.access_ticket_create_ticket_request import (
28+
AccessTicketCreateTicketRequest,
29+
)
30+
31+
anon = Configuration(host=f"{creds.url}/api2/json")
32+
anon.verify_ssl = not creds.insecure
33+
bootstrap = Pmg(anon)
34+
35+
response = bootstrap.accessTicket.create_ticket(
36+
AccessTicketCreateTicketRequest(
37+
username=creds.user,
38+
password=password if password is not None else creds.password,
39+
)
40+
)
41+
data = response.data
42+
if data is None or not data.ticket:
43+
raise RuntimeError(f"ticket login returned no ticket: {response!r}")
44+
return ticket_client(
45+
creds,
46+
ticket=data.ticket,
47+
csrf=data.csrf_prevention_token,
48+
)

e2e/helpers/credentials.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Load PROXMOX_* environment variables exported by client-api/proxmox-docker-action@v1.
2+
3+
PMG has no API tokens, so `PROXMOX_TOKEN_HEADER_VALUE` is optional here.
4+
"""
5+
from __future__ import annotations
6+
7+
import os
8+
from dataclasses import dataclass
9+
10+
11+
class MissingCredentialError(RuntimeError):
12+
"""Raised when a required PROXMOX_* env var is missing."""
13+
14+
15+
@dataclass(frozen=True)
16+
class Credentials:
17+
url: str
18+
user: str
19+
password: str
20+
insecure: bool
21+
22+
@classmethod
23+
def from_env(cls) -> "Credentials":
24+
url = _required("PROXMOX_URL")
25+
return cls(
26+
url=url.rstrip("/"),
27+
user=_required("PROXMOX_USER"),
28+
password=_required("PROXMOX_PASSWORD"),
29+
insecure=os.environ.get("PROXMOX_INSECURE", "").lower() in ("1", "true", "yes"),
30+
)
31+
32+
33+
def _required(name: str) -> str:
34+
value = os.environ.get(name)
35+
if not value:
36+
raise MissingCredentialError(
37+
f"{name} is not set. Run client-api/proxmox-docker-action@v1 in CI "
38+
f"or export it manually for local runs."
39+
)
40+
return value

0 commit comments

Comments
 (0)