Files
docker-images/python-tools/tests/test_web.py
T
ajp_anton b256147d28
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 48s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 1m41s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 1m19s
Refine task form controls
2026-08-18 03:19:07 +00:00

323 lines
10 KiB
Python

from __future__ import annotations
from io import BytesIO
import time
from app.web import create_app
from .conftest import write_task
def test_index_discovers_task_and_run_endpoint(settings) -> None:
write_task(
settings.task_root,
"status_checks",
"show_status.py",
'"""Shows status."""\nprint("healthy")\n',
)
app = create_app(settings)
client = app.test_client()
page = client.get("/")
assert page.status_code == 200
assert b"Status Checks" in page.data
assert b"Shows status." in page.data
response = client.post("/tasks/status_checks/show_status.py/run")
assert response.status_code == 302
assert "/runs/" in response.headers["Location"]
assert client.get(f"{response.headers['Location']}/state").status_code == 200
def test_proxy_allowlist_rejects_other_direct_peers(settings) -> None:
restricted = settings.__class__(
**{**settings.__dict__, "allowed_proxy_ips": frozenset({"192.0.2.1"})}
)
app = create_app(restricted)
assert app.test_client().get("/").status_code == 403
assert app.test_client().get("/", environ_base={"REMOTE_ADDR": "192.0.2.1"}).status_code == 200
def test_task_form_validates_and_persists_typed_inputs(settings) -> None:
task = write_task(
settings.task_root,
"examples",
"queue.py",
'"""Queues work."""\n',
)
(task.parent / "task-inputs.json").write_text(
"""{
"version": 1,
"tasks": {
"queue.py": {
"inputs": [
{"name": "identifier", "label": "Identifier", "type": "text", "required": true, "pattern": "[A-Z]{2}[0-9]+"},
{"name": "days", "label": "Days", "type": "integer", "minimum": 1, "maximum": 7},
{"name": "mode", "label": "Mode", "type": "choice", "required": true, "options": ["standard", "extended"]},
{"name": "alerts", "label": "Alerts", "type": "multi_choice", "options": ["email", "webhook"]}
]
}
}
}""",
encoding="utf-8",
)
app = create_app(settings)
client = app.test_client()
page = client.get("/")
assert b'type="number"' in page.data
assert b'type="checkbox"' in page.data
invalid = client.post(
"/tasks/examples/queue.py/run",
data={"identifier": "bad", "days": "0", "mode": "other"},
)
assert invalid.status_code == 400
assert b"required format" in invalid.data
assert b"at least 1" in invalid.data
assert b"available options" in invalid.data
response = client.post(
"/tasks/examples/queue.py/run",
data={
"identifier": "AB123",
"days": "2",
"mode": "standard",
"alerts": ["email", "webhook"],
},
)
assert response.status_code == 302
run_id = int(response.headers["Location"].rsplit("/", 1)[1])
run = app.extensions["run_store"].get(run_id)
assert run is not None
assert run.input_json == '{"alerts":["email","webhook"],"days":2,"identifier":"AB123","mode":"standard"}'
def test_sensitive_input_uses_a_password_field_and_is_redacted(settings) -> None:
task = write_task(settings.task_root, "examples", "prepare.py", '"""Prepares."""\n')
(task.parent / "task-inputs.json").write_text(
'''{
"version": 1,
"tasks": {
"prepare.py": {
"inputs": [
{"name": "secret", "label": "Secret", "type": "text", "sensitive": true}
]
}
}
}''',
encoding="utf-8",
)
app = create_app(settings)
client = app.test_client()
assert b'name="secret"' in client.get("/").data
assert b'type="password"' in client.get("/").data
response = client.post("/tasks/examples/prepare.py/run", data={"secret": "private-value"})
run_id = int(response.headers["Location"].rsplit("/", 1)[1])
run = app.extensions["run_store"].get(run_id)
assert run is not None
assert run.input_json == '{"secret":"[redacted]"}'
def test_group_with_typed_tasks_cannot_run_as_a_group(settings) -> None:
task = write_task(
settings.task_root,
"examples",
"schedule.py",
'"""Schedules work."""\n',
)
(task.parent / "task-inputs.json").write_text(
'''{
"version": 1,
"tasks": {
"schedule.py": {
"inputs": [
{"name": "identifier", "label": "Identifier", "type": "text"}
]
}
}
}''',
encoding="utf-8",
)
app = create_app(settings)
client = app.test_client()
assert b"Run group" not in client.get("/").data
assert client.post("/groups/examples/run").status_code == 404
def test_wait_for_result_task_uses_json_run_endpoint(settings) -> None:
task = write_task(
settings.task_root,
"examples",
"schedule.py",
'"""Schedules work."""\nprint("scheduled")\n',
)
(task.parent / "task-inputs.json").write_text(
'''{
"version": 1,
"tasks": {
"schedule.py": {
"wait_for_result": true,
"inputs": [
{"name": "identifier", "label": "Identifier", "type": "text", "required": true}
]
}
}
}''',
encoding="utf-8",
)
app = create_app(settings)
client = app.test_client()
assert b"data-wait-for-result" in client.get("/").data
response = client.post(
"/tasks/examples/schedule.py/run",
data={"identifier": "AB123"},
headers={"Accept": "application/json"},
)
assert response.status_code == 202
payload = response.get_json()
assert payload["detail_url"].startswith("/runs/")
assert payload["state_url"].endswith("/state")
invalid = client.post(
"/tasks/examples/schedule.py/run",
data={},
headers={"Accept": "application/json"},
)
assert invalid.status_code == 400
assert invalid.get_json()["errors"] == {"identifier": "This field is required."}
def test_successful_dry_run_can_be_repeated_with_execution_enabled(settings) -> None:
task = write_task(
settings.task_root,
"examples",
"migrate.py",
'''"""Migrates settings."""
import os
from pathlib import Path
print(Path(os.environ["SERVER_MAINTENANCE_INPUT"]).read_text())
''',
)
(task.parent / "task-inputs.json").write_text(
'''{
"version": 1,
"tasks": {
"migrate.py": {
"execution": {
"field": "mode",
"label": "Action",
"dry_run_value": "report",
"dry_run_label": "Report only",
"execute_value": "apply",
"execute_label": "Apply changes"
}
}
}
}''',
encoding="utf-8",
)
app = create_app(settings)
client = app.test_client()
page = client.get("/")
assert b"Report only" in page.data
assert b"Apply changes" in page.data
assert b"data-execution-mode" in page.data
assert b'class="split-button"' in page.data
response = client.post("/tasks/examples/migrate.py/run")
run_id = int(response.headers["Location"].rsplit("/", 1)[1])
deadline = time.monotonic() + 3
while time.monotonic() < deadline:
run = app.extensions["run_store"].get(run_id)
assert run is not None
if run.status == "succeeded":
break
time.sleep(0.01)
else:
raise AssertionError("dry-run task did not finish")
detail = client.get(f"/runs/{run_id}")
assert b"Apply changes" in detail.data
execute = client.post(f"/runs/{run_id}/execute")
assert execute.status_code == 302
executed_id = int(execute.headers["Location"].rsplit("/", 1)[1])
executed = app.extensions["run_store"].get(executed_id)
assert executed is not None
assert executed.input_json == '{"mode":"apply"}'
def test_successful_task_artifact_can_be_downloaded_once(settings) -> None:
task = write_task(
settings.task_root,
"exports",
"review.py",
'''"""Creates a download."""
import os
from pathlib import Path
artifact = Path(os.environ["SERVER_MAINTENANCE_ARTIFACTS"]) / "review.zip"
artifact.write_bytes(b"archive")
print("archive created")
''',
)
app = create_app(settings)
client = app.test_client()
response = client.post("/tasks/exports/review.py/run")
run_id = int(response.headers["Location"].rsplit("/", 1)[1])
deadline = time.monotonic() + 3
while time.monotonic() < deadline:
run = app.extensions["run_store"].get(run_id)
assert run is not None
if run.status == "succeeded":
break
time.sleep(0.01)
else:
raise AssertionError("artifact task did not finish")
assert client.get(f"/runs/{run_id}/output").get_json() == {"output": "archive created\n"}
assert b"archive created" in client.get(f"/runs/{run_id}").data
listed = client.get(f"/runs/{run_id}/artifacts").get_json()
assert listed["artifacts"] == [{"name": "review.zip", "url": f"/runs/{run_id}/artifacts/review.zip"}]
download = client.get(listed["artifacts"][0]["url"])
assert download.data == b"archive"
download.close()
assert app.extensions["run_manager"].artifact_path(run_id, "review.zip") is None
def test_file_input_rejects_the_wrong_extension(settings) -> None:
task = write_task(settings.task_root, "imports", "apply.py", '"""Applies a CSV."""\n')
(task.parent / "task-inputs.json").write_text(
'''{
"version": 1,
"tasks": {
"apply.py": {
"inputs": [
{"name": "review_csv", "label": "Review CSV", "type": "file", "required": true, "accept": [".csv"], "maximum_bytes": 1024}
]
}
}
}''',
encoding="utf-8",
)
app = create_app(settings)
client = app.test_client()
response = client.post(
"/tasks/imports/apply.py/run",
data={"review_csv": (BytesIO(b"not csv"), "review.txt")},
headers={"Accept": "application/json"},
)
assert response.status_code == 400
assert response.get_json()["errors"] == {"review_csv": "Choose a file with one of: .csv."}