Files
gomag-vending/api/tests/qa/test_qa_smoke_prod.py
Claude Agent 419464a62c feat: add CI/CD testing infrastructure with test.sh orchestrator
Complete testing system: pyproject.toml (pytest markers), test.sh
orchestrator with auto app start/stop and colorful summary,
pre-push hook, Gitea Actions workflow.

New QA tests: API health (7 endpoints), responsive (3 viewports),
log monitoring (ERROR/ORA-/Traceback detection), real GoMag sync,
PL/SQL package validation, smoke prod (read-only).

Converted test_app_basic.py and test_integration.py to pytest.
Added pytestmark to all existing tests (unit/e2e/oracle).
E2E conftest upgraded: console error collector, screenshot on
failure, auto-detect live app on :5003.

Usage: ./test.sh ci (30s) | ./test.sh full (2-3min)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:40:25 +00:00

143 lines
4.9 KiB
Python

"""
Smoke tests for production — read-only, no clicks.
Run against a live app: pytest api/tests/qa/test_qa_smoke_prod.py --base-url http://localhost:5003
"""
import time
import urllib.request
import json
import pytest
from playwright.sync_api import sync_playwright
pytestmark = pytest.mark.smoke
PAGES = ["/", "/logs", "/mappings", "/missing-skus", "/settings"]
def _app_is_reachable(base_url: str) -> bool:
"""Quick check if the app is reachable."""
try:
urllib.request.urlopen(f"{base_url}/health", timeout=3)
return True
except Exception:
return False
@pytest.fixture(scope="module", autouse=True)
def _require_app(base_url):
"""Skip all smoke tests if the app is not running."""
if not _app_is_reachable(base_url):
pytest.skip(f"App not reachable at {base_url} — start the app first")
PAGE_TITLES = {
"/": "Panou de Comanda",
"/logs": "Jurnale Import",
"/mappings": "Mapari SKU",
"/missing-skus": "SKU-uri Lipsa",
"/settings": "Setari",
}
@pytest.fixture(scope="module")
def browser():
with sync_playwright() as p:
b = p.chromium.launch(headless=True)
yield b
b.close()
# ---------------------------------------------------------------------------
# test_page_loads
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", PAGES)
def test_page_loads(browser, base_url, screenshots_dir, path):
"""Each page returns HTTP 200 and loads without crashing."""
page = browser.new_page()
try:
response = page.goto(f"{base_url}{path}", wait_until="domcontentloaded", timeout=15_000)
assert response is not None, f"No response for {path}"
assert response.status == 200, f"Expected 200, got {response.status} for {path}"
safe_name = path.strip("/").replace("/", "_") or "dashboard"
screenshot_path = screenshots_dir / f"smoke_{safe_name}.png"
page.screenshot(path=str(screenshot_path))
finally:
page.close()
# ---------------------------------------------------------------------------
# test_page_titles
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", PAGES)
def test_page_titles(browser, base_url, path):
"""Each page has the correct h4 heading text."""
expected = PAGE_TITLES[path]
page = browser.new_page()
try:
page.goto(f"{base_url}{path}", wait_until="domcontentloaded", timeout=15_000)
h4 = page.locator("h4").first
actual = h4.inner_text().strip()
assert actual == expected, f"{path}: expected h4='{expected}', got '{actual}'"
finally:
page.close()
# ---------------------------------------------------------------------------
# test_no_console_errors
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", PAGES)
def test_no_console_errors(browser, base_url, path):
"""No console.error events on any page."""
errors = []
page = browser.new_page()
try:
page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None)
page.goto(f"{base_url}{path}", wait_until="networkidle", timeout=15_000)
finally:
page.close()
assert errors == [], f"Console errors on {path}: {errors}"
# ---------------------------------------------------------------------------
# test_api_health_json
# ---------------------------------------------------------------------------
def test_api_health_json(base_url):
"""GET /health returns valid JSON with 'oracle' key."""
with urllib.request.urlopen(f"{base_url}/health", timeout=10) as resp:
data = json.loads(resp.read().decode())
assert "oracle" in data, f"/health JSON missing 'oracle' key: {data}"
# ---------------------------------------------------------------------------
# test_api_dashboard_orders_json
# ---------------------------------------------------------------------------
def test_api_dashboard_orders_json(base_url):
"""GET /api/dashboard/orders returns valid JSON with 'orders' key."""
with urllib.request.urlopen(f"{base_url}/api/dashboard/orders", timeout=10) as resp:
data = json.loads(resp.read().decode())
assert "orders" in data, f"/api/dashboard/orders JSON missing 'orders' key: {data}"
# ---------------------------------------------------------------------------
# test_response_time
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("path", PAGES)
def test_response_time(browser, base_url, path):
"""Each page loads in under 10 seconds."""
page = browser.new_page()
try:
start = time.monotonic()
page.goto(f"{base_url}{path}", wait_until="domcontentloaded", timeout=15_000)
elapsed = time.monotonic() - start
finally:
page.close()
assert elapsed < 10, f"{path} took {elapsed:.2f}s (limit: 10s)"