Files
matomo-bootstrap/tests/e2e/test_docker_compose_stack.py
Kevin Veen-Birkenbach 7fa8b580d2
Some checks failed
ci / tests (push) Has been cancelled
test(e2e): validate root docker-compose stack bootstrap flow
Adds an end-to-end test that brings up the root docker-compose.yml stack,
runs the one-shot bootstrap container, verifies token-only stdout, and
checks the token via Matomo API, with full cleanup via down -v.

https://chatgpt.com/share/694af650-a484-800f-ace7-0a634d57b0a0
2025-12-23 21:06:16 +01:00

108 lines
3.5 KiB
Python

import json
import os
import re
import subprocess
import time
import unittest
import urllib.request
COMPOSE_FILE = os.environ.get("MATOMO_STACK_COMPOSE_FILE", "docker-compose.yml")
MATOMO_HOST_URL = os.environ.get("MATOMO_STACK_URL", "http://127.0.0.1:8080")
# How long we wait for Matomo HTTP to respond at all (seconds)
WAIT_TIMEOUT_SECONDS = int(os.environ.get("MATOMO_STACK_WAIT_TIMEOUT", "180"))
def _run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess:
return subprocess.run(
cmd,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
def _compose_cmd(*args: str) -> list[str]:
return ["docker", "compose", "-f", COMPOSE_FILE, *args]
def _wait_for_http_any_status(url: str, timeout_s: int) -> None:
"""
Consider the service "up" once the HTTP server answers anything.
urllib raises HTTPError on 4xx/5xx, but that's still "reachable".
"""
deadline = time.time() + timeout_s
last_exc: Exception | None = None
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=2) as resp:
_ = resp.read(64)
return
except Exception as exc: # includes HTTPError
last_exc = exc
time.sleep(1)
raise RuntimeError(f"Matomo did not become reachable at {url} ({last_exc})")
class TestRootDockerComposeStack(unittest.TestCase):
"""
E2E test for repository root docker-compose.yml:
1) docker compose down -v
2) docker compose build bootstrap
3) docker compose up -d db matomo
4) wait for Matomo HTTP on host port (default 8080)
5) docker compose run --rm bootstrap -> token on stdout
6) validate token via Matomo API call
7) docker compose down -v (cleanup)
"""
def setUp(self) -> None:
# Always start from a clean slate (also clears volumes)
_run(_compose_cmd("down", "-v"), check=False)
def tearDown(self) -> None:
# Cleanup even if assertions fail
_run(_compose_cmd("down", "-v"), check=False)
def test_root_docker_compose_yml_stack_bootstraps_and_token_works(self) -> None:
# Build bootstrap image from Dockerfile (as defined in docker-compose.yml)
build = _run(_compose_cmd("build", "bootstrap"), check=True)
self.assertEqual(build.returncode, 0, build.stderr)
# Start db + matomo (bootstrap is one-shot and started via "run")
up = _run(_compose_cmd("up", "-d", "db", "matomo"), check=True)
self.assertEqual(up.returncode, 0, up.stderr)
# Wait until Matomo answers on the published port
_wait_for_http_any_status(MATOMO_HOST_URL + "/", WAIT_TIMEOUT_SECONDS)
# Run bootstrap: it should print ONLY the token to stdout
boot = _run(_compose_cmd("run", "--rm", "bootstrap"), check=True)
token = (boot.stdout or "").strip()
self.assertRegex(
token,
r"^[a-f0-9]{32,64}$",
f"Expected token_auth on stdout, got stdout={boot.stdout!r} stderr={boot.stderr!r}",
)
# Verify token works against Matomo API
api_url = (
f"{MATOMO_HOST_URL}/index.php"
f"?module=API&method=SitesManager.getSitesWithAtLeastViewAccess"
f"&format=json&token_auth={token}"
)
with urllib.request.urlopen(api_url, timeout=10) as resp:
data = json.loads(resp.read().decode("utf-8", errors="replace"))
self.assertIsInstance(data, list)
if __name__ == "__main__":
unittest.main()