From 65e26014e37042a554cf8b787b6dd84ab826e31e Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 23 Dec 2025 11:36:20 +0100 Subject: [PATCH] test(e2e): add venv + playwright deps and robust Matomo API auth flow - Add optional dependency group "e2e" with Playwright in pyproject.toml - Teach Makefile to create/use local .venv, install e2e deps, and fetch Chromium - Wait for Matomo HTTP response (any status) before running bootstrap - Switch API calls to /index.php and add HttpClient.get() - Use UsersManager.getTokenAuth (md5Password) to obtain token_auth for privileged calls - Make web installer more resilient to HTTPError/500 and locale/button variations - Update E2E test to pass admin email and call API via /index.php https://chatgpt.com/share/694a70b0-e520-800f-a3e4-eaf5e96530bd --- Makefile | 65 ++++++--- pyproject.toml | 9 +- src/matomo_bootstrap.egg-info/PKG-INFO | 20 +++ src/matomo_bootstrap.egg-info/SOURCES.txt | 19 +++ .../dependency_links.txt | 1 + src/matomo_bootstrap.egg-info/requires.txt | 3 + src/matomo_bootstrap.egg-info/top_level.txt | 1 + src/matomo_bootstrap/api_tokens.py | 55 +++++++- src/matomo_bootstrap/bootstrap.py | 20 ++- src/matomo_bootstrap/cli.py | 49 ++++++- src/matomo_bootstrap/http.py | 16 ++- .../install/container_installer.py | 34 +++++ src/matomo_bootstrap/install/web_installer.py | 123 ++++++++++-------- tests/e2e/test_bootstrap.py | 5 +- 14 files changed, 325 insertions(+), 95 deletions(-) create mode 100644 src/matomo_bootstrap.egg-info/PKG-INFO create mode 100644 src/matomo_bootstrap.egg-info/SOURCES.txt create mode 100644 src/matomo_bootstrap.egg-info/dependency_links.txt create mode 100644 src/matomo_bootstrap.egg-info/requires.txt create mode 100644 src/matomo_bootstrap.egg-info/top_level.txt create mode 100644 src/matomo_bootstrap/install/container_installer.py diff --git a/Makefile b/Makefile index a7714a8..c884fea 100644 --- a/Makefile +++ b/Makefile @@ -2,37 +2,71 @@ PYTHON ?= python3 COMPOSE_FILE := tests/e2e/docker-compose.yml COMPOSE := docker compose -f $(COMPOSE_FILE) -.PHONY: help e2e-up e2e-install e2e-test e2e-down e2e logs clean +VENV_DIR := .venv +VENV_PY := $(VENV_DIR)/bin/python +VENV_PIP := $(VENV_DIR)/bin/pip + +# E2E defaults (override like: make e2e MATOMO_URL=http://127.0.0.1:8081) +MATOMO_URL ?= http://127.0.0.1:8080 +MATOMO_ADMIN_USER ?= administrator +MATOMO_ADMIN_PASSWORD ?= AdminSecret123! +MATOMO_ADMIN_EMAIL ?= administrator@example.org +MATOMO_TOKEN_DESCRIPTION ?= e2e-make-token + +.PHONY: help venv deps-e2e playwright-install e2e-up e2e-install e2e-test e2e-down e2e logs clean help: @echo "Targets:" - @echo " e2e-up Start Matomo + DB for E2E tests" - @echo " e2e-install Run Matomo installation (product code)" - @echo " e2e-test Run E2E tests (unittest)" - @echo " e2e-down Stop and remove E2E containers" - @echo " e2e Full cycle: up → install → test → down" - @echo " logs Show Matomo logs" + @echo " venv Create local venv in $(VENV_DIR)" + @echo " deps-e2e Install package + E2E deps into venv" + @echo " playwright-install Install Chromium for Playwright (inside venv)" + @echo " e2e-up Start Matomo + DB for E2E tests" + @echo " e2e-install Run Matomo bootstrap (product code)" + @echo " e2e-test Run E2E tests (unittest)" + @echo " e2e-down Stop and remove E2E containers" + @echo " e2e Full cycle: up → install → test → down" + @echo " logs Show Matomo logs" + @echo " clean Stop containers + remove venv" + @echo "" + @echo "Variables (override like: make e2e MATOMO_URL=http://127.0.0.1:8081):" + @echo " MATOMO_URL, MATOMO_ADMIN_USER, MATOMO_ADMIN_PASSWORD, MATOMO_ADMIN_EMAIL, MATOMO_TOKEN_DESCRIPTION" + +venv: + @test -x "$(VENV_PY)" || ($(PYTHON) -m venv $(VENV_DIR)) + @$(VENV_PIP) -q install -U pip setuptools wheel >/dev/null + +deps-e2e: venv + @$(VENV_PIP) install -e ".[e2e]" + +playwright-install: deps-e2e + @$(VENV_PY) -m playwright install chromium e2e-up: $(COMPOSE) up -d - @echo "Waiting for Matomo to answer on http://127.0.0.1:8080/ ..." + @echo "Waiting for Matomo to answer (any HTTP code) on $(MATOMO_URL)/ ..." @for i in $$(seq 1 180); do \ - if curl -fsS --max-time 2 http://127.0.0.1:8080/ >/dev/null 2>&1; then \ - echo "Matomo is reachable."; \ + code=$$(curl -sS -o /dev/null -w "%{http_code}" --max-time 2 "$(MATOMO_URL)/" || true); \ + if [ "$$code" != "000" ]; then \ + echo "Matomo answered with HTTP $$code."; \ exit 0; \ fi; \ sleep 1; \ done; \ - echo "Matomo did not become reachable on host port 8080."; \ + echo "Matomo did not answer on $(MATOMO_URL)"; \ $(COMPOSE) ps; \ $(COMPOSE) logs --no-color --tail=200 matomo; \ exit 1 -e2e-install: - PYTHONPATH=src $(PYTHON) -m matomo_bootstrap.install.web_installer +e2e-install: playwright-install + MATOMO_URL="$(MATOMO_URL)" \ + MATOMO_ADMIN_USER="$(MATOMO_ADMIN_USER)" \ + MATOMO_ADMIN_PASSWORD="$(MATOMO_ADMIN_PASSWORD)" \ + MATOMO_ADMIN_EMAIL="$(MATOMO_ADMIN_EMAIL)" \ + MATOMO_TOKEN_DESCRIPTION="$(MATOMO_TOKEN_DESCRIPTION)" \ + PYTHONPATH=src $(VENV_PY) -m matomo_bootstrap -e2e-test: - PYTHONPATH=src $(PYTHON) -m unittest discover -s tests/e2e -v +e2e-test: deps-e2e + PYTHONPATH=src $(VENV_PY) -m unittest discover -s tests/e2e -v e2e-down: $(COMPOSE) down -v @@ -43,3 +77,4 @@ logs: $(COMPOSE) logs -f matomo clean: e2e-down + rm -rf $(VENV_DIR) diff --git a/pyproject.toml b/pyproject.toml index 3b039a9..73f010e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "matomo-bootstrap" version = "0.1.0" -description = "" +description = "Headless bootstrap tooling for Matomo (installation + API token provisioning)" readme = "README.md" requires-python = ">=3.10" authors = [{ name = "Kevin Veen-Birkenbach", email = "kevin@veen.world" }] @@ -14,8 +14,13 @@ urls = { Homepage = "https://github.com/kevinveenbirkenbach/matomo-bootstrap" } dependencies = [] +[project.optional-dependencies] +e2e = [ + "playwright>=1.40.0", +] + [tool.setuptools] -package-dir = {"" = "src"} +package-dir = { "" = "src" } [tool.setuptools.packages.find] where = ["src"] diff --git a/src/matomo_bootstrap.egg-info/PKG-INFO b/src/matomo_bootstrap.egg-info/PKG-INFO new file mode 100644 index 0000000..db5a6ad --- /dev/null +++ b/src/matomo_bootstrap.egg-info/PKG-INFO @@ -0,0 +1,20 @@ +Metadata-Version: 2.4 +Name: matomo-bootstrap +Version: 0.1.0 +Summary: Headless bootstrap tooling for Matomo (installation + API token provisioning) +Author-email: Kevin Veen-Birkenbach +License: All rights reserved by Kevin Veen-Birkenbach +Project-URL: Homepage, https://github.com/kevinveenbirkenbach/matomo-bootstrap +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: e2e +Requires-Dist: playwright>=1.40.0; extra == "e2e" +Dynamic: license-file + +# matomo-bootstrap + +Homepage: https://github.com/kevinveenbirkenbach/matomo-bootstrap + +## Author +Kevin Veen-Birkenbach diff --git a/src/matomo_bootstrap.egg-info/SOURCES.txt b/src/matomo_bootstrap.egg-info/SOURCES.txt new file mode 100644 index 0000000..2d21f88 --- /dev/null +++ b/src/matomo_bootstrap.egg-info/SOURCES.txt @@ -0,0 +1,19 @@ +LICENSE +README.md +pyproject.toml +src/matomo_bootstrap/__init__.py +src/matomo_bootstrap/__main__.py +src/matomo_bootstrap/api_tokens.py +src/matomo_bootstrap/bootstrap.py +src/matomo_bootstrap/cli.py +src/matomo_bootstrap/errors.py +src/matomo_bootstrap/health.py +src/matomo_bootstrap/http.py +src/matomo_bootstrap.egg-info/PKG-INFO +src/matomo_bootstrap.egg-info/SOURCES.txt +src/matomo_bootstrap.egg-info/dependency_links.txt +src/matomo_bootstrap.egg-info/requires.txt +src/matomo_bootstrap.egg-info/top_level.txt +src/matomo_bootstrap/install/__init__.py +src/matomo_bootstrap/install/container_installer.py +src/matomo_bootstrap/install/web_installer.py \ No newline at end of file diff --git a/src/matomo_bootstrap.egg-info/dependency_links.txt b/src/matomo_bootstrap.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/matomo_bootstrap.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/matomo_bootstrap.egg-info/requires.txt b/src/matomo_bootstrap.egg-info/requires.txt new file mode 100644 index 0000000..286c671 --- /dev/null +++ b/src/matomo_bootstrap.egg-info/requires.txt @@ -0,0 +1,3 @@ + +[e2e] +playwright>=1.40.0 diff --git a/src/matomo_bootstrap.egg-info/top_level.txt b/src/matomo_bootstrap.egg-info/top_level.txt new file mode 100644 index 0000000..2807f30 --- /dev/null +++ b/src/matomo_bootstrap.egg-info/top_level.txt @@ -0,0 +1 @@ +matomo_bootstrap diff --git a/src/matomo_bootstrap/api_tokens.py b/src/matomo_bootstrap/api_tokens.py index 67d5a97..69dedec 100644 --- a/src/matomo_bootstrap/api_tokens.py +++ b/src/matomo_bootstrap/api_tokens.py @@ -1,16 +1,58 @@ +import hashlib import json -from .http import HttpClient from .errors import TokenCreationError +from .http import HttpClient + + +def get_token_auth(client: HttpClient, admin_user: str, admin_password: str) -> str: + """ + Get the user's token_auth via UsersManager.getTokenAuth. + + This is the most robust way to authenticate subsequent API calls without relying + on UI sessions/cookies. + """ + md5_password = hashlib.md5(admin_password.encode("utf-8")).hexdigest() + + status, body = client.get( + "/index.php", + { + "module": "API", + "method": "UsersManager.getTokenAuth", + "userLogin": admin_user, + "md5Password": md5_password, + "format": "json", + }, + ) + + if status != 200: + raise TokenCreationError(f"HTTP {status} during getTokenAuth: {body[:200]}") + + try: + data = json.loads(body) + except json.JSONDecodeError as exc: + raise TokenCreationError(f"Invalid JSON from getTokenAuth: {body[:200]}") from exc + + # Matomo returns either {"value": "..."} or sometimes a plain string depending on setup/version + if isinstance(data, dict) and data.get("value"): + return str(data["value"]) + if isinstance(data, str) and data: + return data + + raise TokenCreationError(f"Unexpected getTokenAuth response: {data}") def create_app_token( client: HttpClient, + admin_token_auth: str, admin_user: str, admin_password: str, description: str, ) -> str: + """ + Create an app-specific token using token_auth authentication. + """ status, body = client.post( - "/api.php", + "/index.php", { "module": "API", "method": "UsersManager.createAppSpecificTokenAuth", @@ -18,19 +60,20 @@ def create_app_token( "passwordConfirmation": admin_password, "description": description, "format": "json", + "token_auth": admin_token_auth, }, ) if status != 200: - raise TokenCreationError(f"HTTP {status} during token creation") + raise TokenCreationError(f"HTTP {status} during token creation: {body[:200]}") try: data = json.loads(body) except json.JSONDecodeError as exc: - raise TokenCreationError("Invalid JSON from Matomo API") from exc + raise TokenCreationError(f"Invalid JSON from Matomo API: {body[:200]}") from exc - token = data.get("value") + token = data.get("value") if isinstance(data, dict) else None if not token: raise TokenCreationError(f"Unexpected response: {data}") - return token + return str(token) diff --git a/src/matomo_bootstrap/bootstrap.py b/src/matomo_bootstrap/bootstrap.py index 0d24ea4..f2e0437 100644 --- a/src/matomo_bootstrap/bootstrap.py +++ b/src/matomo_bootstrap/bootstrap.py @@ -1,15 +1,13 @@ from argparse import Namespace + +from .api_tokens import create_app_token, get_token_auth from .health import assert_matomo_ready from .http import HttpClient -from .api_tokens import create_app_token from .install.web_installer import ensure_installed def run_bootstrap(args: Namespace) -> str: - # 1. Matomo erreichbar? - assert_matomo_ready(args.base_url, timeout=args.timeout) - - # 2. Installation sicherstellen (NO-OP wenn bereits installiert) + # 1) Ensure Matomo is installed (NO-OP if already installed) ensure_installed( base_url=args.base_url, admin_user=args.admin_user, @@ -18,15 +16,25 @@ def run_bootstrap(args: Namespace) -> str: debug=args.debug, ) - # 3. API-Token erzeugen + # 2) Now the UI/API should be reachable and "installed" + assert_matomo_ready(args.base_url, timeout=args.timeout) + + # 3) Create authenticated API token flow (no UI session needed) client = HttpClient( base_url=args.base_url, timeout=args.timeout, debug=args.debug, ) + admin_token_auth = get_token_auth( + client=client, + admin_user=args.admin_user, + admin_password=args.admin_password, + ) + token = create_app_token( client=client, + admin_token_auth=admin_token_auth, admin_user=args.admin_user, admin_password=args.admin_password, description=args.token_description, diff --git a/src/matomo_bootstrap/cli.py b/src/matomo_bootstrap/cli.py index acf5d84..716007c 100644 --- a/src/matomo_bootstrap/cli.py +++ b/src/matomo_bootstrap/cli.py @@ -1,4 +1,5 @@ import argparse +import os def parse_args() -> argparse.Namespace: @@ -6,12 +7,46 @@ def parse_args() -> argparse.Namespace: description="Headless bootstrap tool for Matomo (installation + API token provisioning)" ) - p.add_argument("--base-url", required=True, help="Matomo base URL") - p.add_argument("--admin-user", required=True) - p.add_argument("--admin-password", required=True) - p.add_argument("--admin-email", required=True) - p.add_argument("--token-description", default="matomo-bootstrap") - p.add_argument("--timeout", type=int, default=20) + p.add_argument( + "--base-url", + default=os.environ.get("MATOMO_URL"), + help="Matomo base URL (or MATOMO_URL env)", + ) + p.add_argument( + "--admin-user", + default=os.environ.get("MATOMO_ADMIN_USER"), + help="Admin login (or MATOMO_ADMIN_USER env)", + ) + p.add_argument( + "--admin-password", + default=os.environ.get("MATOMO_ADMIN_PASSWORD"), + help="Admin password (or MATOMO_ADMIN_PASSWORD env)", + ) + p.add_argument( + "--admin-email", + default=os.environ.get("MATOMO_ADMIN_EMAIL"), + help="Admin email (or MATOMO_ADMIN_EMAIL env)", + ) + p.add_argument( + "--token-description", + default=os.environ.get("MATOMO_TOKEN_DESCRIPTION", "matomo-bootstrap"), + ) + p.add_argument("--timeout", type=int, default=int(os.environ.get("MATOMO_TIMEOUT", "20"))) p.add_argument("--debug", action="store_true") - return p.parse_args() + args = p.parse_args() + + missing = [] + if not args.base_url: + missing.append("--base-url (or MATOMO_URL)") + if not args.admin_user: + missing.append("--admin-user (or MATOMO_ADMIN_USER)") + if not args.admin_password: + missing.append("--admin-password (or MATOMO_ADMIN_PASSWORD)") + if not args.admin_email: + missing.append("--admin-email (or MATOMO_ADMIN_EMAIL)") + + if missing: + p.error("missing required values: " + ", ".join(missing)) + + return args diff --git a/src/matomo_bootstrap/http.py b/src/matomo_bootstrap/http.py index 3541ed7..dc7e4a7 100644 --- a/src/matomo_bootstrap/http.py +++ b/src/matomo_bootstrap/http.py @@ -1,6 +1,6 @@ -import urllib.request -import urllib.parse import http.cookiejar +import urllib.parse +import urllib.request from typing import Dict, Tuple @@ -15,6 +15,18 @@ class HttpClient: urllib.request.HTTPCookieProcessor(self.cookies) ) + def get(self, path: str, params: Dict[str, str]) -> Tuple[int, str]: + qs = urllib.parse.urlencode(params) + url = f"{self.base_url}{path}?{qs}" + + if self.debug: + print(f"[HTTP] GET {url}") + + req = urllib.request.Request(url, method="GET") + with self.opener.open(req, timeout=self.timeout) as resp: + body = resp.read().decode("utf-8", errors="replace") + return resp.status, body + def post(self, path: str, data: Dict[str, str]) -> Tuple[int, str]: url = self.base_url + path encoded = urllib.parse.urlencode(data).encode() diff --git a/src/matomo_bootstrap/install/container_installer.py b/src/matomo_bootstrap/install/container_installer.py new file mode 100644 index 0000000..6af2192 --- /dev/null +++ b/src/matomo_bootstrap/install/container_installer.py @@ -0,0 +1,34 @@ +import subprocess + + +def ensure_installed( + container_name: str = "e2e-matomo-1", +) -> None: + """ + Ensure Matomo is installed by executing PHP bootstrap inside container. + Idempotent: safe to run multiple times. + """ + + cmd = [ + "docker", + "exec", + container_name, + "php", + "-r", + r""" + if (file_exists('/var/www/html/config/config.ini.php')) { + echo "Matomo already installed\n"; + exit(0); + } + + require '/var/www/html/core/bootstrap.php'; + + \Piwik\FrontController::getInstance()->init(); + \Piwik\Plugins\Installation\Installation::install(); + + echo "Matomo installed\n"; + """ + ] + + subprocess.check_call(cmd) + diff --git a/src/matomo_bootstrap/install/web_installer.py b/src/matomo_bootstrap/install/web_installer.py index c5db7ba..6a85f41 100644 --- a/src/matomo_bootstrap/install/web_installer.py +++ b/src/matomo_bootstrap/install/web_installer.py @@ -1,14 +1,10 @@ import os import sys import time +import urllib.error import urllib.request -MATOMO_URL = os.environ.get("MATOMO_URL", "http://127.0.0.1:8080") -ADMIN_USER = os.environ.get("MATOMO_ADMIN_USER", "administrator") -ADMIN_PASSWORD = os.environ.get("MATOMO_ADMIN_PASSWORD", "AdminSecret123!") -ADMIN_EMAIL = os.environ.get("MATOMO_ADMIN_EMAIL", "admin@example.org") - DB_HOST = os.environ.get("MATOMO_DB_HOST", "db") DB_USER = os.environ.get("MATOMO_DB_USER", "matomo") DB_PASS = os.environ.get("MATOMO_DB_PASS", "matomo_pw") @@ -17,25 +13,49 @@ DB_PREFIX = os.environ.get("MATOMO_DB_PREFIX", "matomo_") def wait_http(url: str, timeout: int = 180) -> None: + """ + Consider Matomo 'reachable' as soon as the HTTP server answers - even with 500. + urllib raises HTTPError for 4xx/5xx, so we must treat that as reachability too. + """ print(f"[install] Waiting for Matomo HTTP at {url} ...") + last_err: Exception | None = None + for i in range(timeout): try: with urllib.request.urlopen(url, timeout=2) as resp: - _ = resp.read(1024) - print("[install] Matomo HTTP reachable.") + _ = resp.read(128) + print("[install] Matomo HTTP reachable (2xx/3xx).") return - except Exception: + except urllib.error.HTTPError as exc: + # 4xx/5xx means the server answered -> reachable + print(f"[install] Matomo HTTP reachable (HTTP {exc.code}).") + return + except Exception as exc: + last_err = exc if i % 5 == 0: - print(f"[install] still waiting ({i}/{timeout}) …") + print(f"[install] still waiting ({i}/{timeout}) … ({type(exc).__name__})") time.sleep(1) - raise RuntimeError(f"Matomo did not become reachable after {timeout}s: {url}") + + raise RuntimeError(f"Matomo did not become reachable after {timeout}s: {url} ({last_err})") def is_installed(url: str) -> bool: + """ + Heuristic: + - installed instances typically render login module links + - installer renders 'installation' wizard content + """ try: - with urllib.request.urlopen(url, timeout=3) as resp: + with urllib.request.urlopen(url, timeout=5) as resp: html = resp.read().decode(errors="ignore").lower() - return ("module=login" in html) or ("matomo › login" in html) + return ("module=login" in html) or ("matomo › login" in html) or ("matomo/login" in html) + except urllib.error.HTTPError as exc: + # Even if it's 500, read body and try heuristic. + try: + html = exc.read().decode(errors="ignore").lower() + return ("module=login" in html) or ("matomo › login" in html) or ("matomo/login" in html) + except Exception: + return False except Exception: return False @@ -51,24 +71,12 @@ def ensure_installed( Ensure Matomo is installed. NO-OP if already installed. """ + wait_http(base_url) - # Propagate config to installer via ENV (single source of truth) - os.environ["MATOMO_URL"] = base_url - os.environ["MATOMO_ADMIN_USER"] = admin_user - os.environ["MATOMO_ADMIN_PASSWORD"] = admin_password - os.environ["MATOMO_ADMIN_EMAIL"] = admin_email - - rc = main() - if rc != 0: - raise RuntimeError("Matomo installation failed") - - -def main() -> int: - wait_http(MATOMO_URL) - - if is_installed(MATOMO_URL): - print("[install] Matomo already installed. Skipping installer.") - return 0 + if is_installed(base_url): + if debug: + print("[install] Matomo already looks installed. Skipping web installer.") + return try: from playwright.sync_api import sync_playwright @@ -78,32 +86,40 @@ def main() -> int: "Install with: python3 -m pip install playwright && python3 -m playwright install chromium", file=sys.stderr, ) - print(f"Reason: {exc}", file=sys.stderr) - return 2 + raise RuntimeError(f"Playwright missing: {exc}") from exc print("[install] Running Matomo web installer via headless browser...") with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() - page.goto(MATOMO_URL, wait_until="domcontentloaded") - def click_next(): - for label in ["Next", "Continue", "Start Installation", "Proceed"]: + # Load installer (may be 500 in curl, but browser can still render the Matomo error/installer flow) + page.goto(base_url, wait_until="domcontentloaded") + + def click_next() -> None: + # Buttons vary slightly with locales/versions + for label in ["Next", "Continue", "Start Installation", "Proceed", "Weiter", "Fortfahren"]: btn = page.get_by_role("button", name=label) if btn.count() > 0: btn.first.click() - return True - return False + return + # Sometimes it's a link styled as button + for text in ["Next", "Continue", "Start Installation", "Proceed", "Weiter", "Fortfahren"]: + a = page.get_by_text(text, exact=False) + if a.count() > 0: + a.first.click() + return + raise RuntimeError("Could not find a 'Next/Continue' control in installer UI.") - # Welcome / system check - page.wait_for_timeout(500) + # Welcome / System check + page.wait_for_timeout(700) click_next() - page.wait_for_timeout(500) + page.wait_for_timeout(700) click_next() # Database setup - page.wait_for_timeout(500) + page.wait_for_timeout(700) page.get_by_label("Database Server").fill(DB_HOST) page.get_by_label("Login").fill(DB_USER) page.get_by_label("Password").fill(DB_PASS) @@ -115,22 +131,22 @@ def main() -> int: click_next() # Tables creation - page.wait_for_timeout(500) + page.wait_for_timeout(700) click_next() # Super user - page.wait_for_timeout(500) - page.get_by_label("Login").fill(ADMIN_USER) - page.get_by_label("Password").fill(ADMIN_PASSWORD) + page.wait_for_timeout(700) + page.get_by_label("Login").fill(admin_user) + page.get_by_label("Password").fill(admin_password) try: - page.get_by_label("Password (repeat)").fill(ADMIN_PASSWORD) + page.get_by_label("Password (repeat)").fill(admin_password) except Exception: pass - page.get_by_label("Email").fill(ADMIN_EMAIL) + page.get_by_label("Email").fill(admin_email) click_next() # First website - page.wait_for_timeout(500) + page.wait_for_timeout(700) try: page.get_by_label("Name").fill("Bootstrap Site") except Exception: @@ -142,19 +158,14 @@ def main() -> int: click_next() # Finish - page.wait_for_timeout(500) + page.wait_for_timeout(700) click_next() browser.close() + # Verify installed time.sleep(2) - if not is_installed(MATOMO_URL): - print("[install] Installer did not reach installed state.", file=sys.stderr) - return 3 + if not is_installed(base_url): + raise RuntimeError("[install] Installer did not reach installed state.") print("[install] Installation finished.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/e2e/test_bootstrap.py b/tests/e2e/test_bootstrap.py index bb849f5..13824bc 100644 --- a/tests/e2e/test_bootstrap.py +++ b/tests/e2e/test_bootstrap.py @@ -8,6 +8,7 @@ import urllib.request MATOMO_URL = os.environ.get("MATOMO_URL", "http://127.0.0.1:8080") ADMIN_USER = os.environ.get("MATOMO_ADMIN_USER", "administrator") ADMIN_PASSWORD = os.environ.get("MATOMO_ADMIN_PASSWORD", "AdminSecret123!") +ADMIN_EMAIL = os.environ.get("MATOMO_ADMIN_EMAIL", "administrator@example.org") class TestMatomoBootstrapE2E(unittest.TestCase): @@ -22,6 +23,8 @@ class TestMatomoBootstrapE2E(unittest.TestCase): ADMIN_USER, "--admin-password", ADMIN_PASSWORD, + "--admin-email", + ADMIN_EMAIL, "--token-description", "e2e-test-token", ] @@ -34,7 +37,7 @@ class TestMatomoBootstrapE2E(unittest.TestCase): self.assertRegex(token, r"^[a-f0-9]{32,64}$", f"Expected token_auth, got: {token}") api_url = ( - f"{MATOMO_URL}/api.php" + f"{MATOMO_URL}/index.php" f"?module=API&method=SitesManager.getSitesWithAtLeastViewAccess" f"&format=json&token_auth={token}" )