Files
gomag-vending/api/tests/test_app_basic.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

115 lines
3.4 KiB
Python

"""
Test: Basic App Import and Route Tests (pytest-compatible)
==========================================================
Tests module imports and all GET routes without requiring Oracle.
Converted from api/test_app_basic.py.
Run:
pytest api/tests/test_app_basic.py -v
"""
import os
import sys
import tempfile
import pytest
# --- Marker: all tests here are unit (no Oracle) ---
pytestmark = pytest.mark.unit
# --- Set env vars BEFORE any app import ---
_tmpdir = tempfile.mkdtemp()
_sqlite_path = os.path.join(_tmpdir, "test_import.db")
os.environ["FORCE_THIN_MODE"] = "true"
os.environ["SQLITE_DB_PATH"] = _sqlite_path
os.environ["ORACLE_DSN"] = "dummy"
os.environ["ORACLE_USER"] = "dummy"
os.environ["ORACLE_PASSWORD"] = "dummy"
os.environ.setdefault("JSON_OUTPUT_DIR", _tmpdir)
# Add api/ to path so we can import app
_api_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _api_dir not in sys.path:
sys.path.insert(0, _api_dir)
# -------------------------------------------------------
# Section 1: Module Import Checks
# -------------------------------------------------------
MODULES = [
"app.config",
"app.database",
"app.main",
"app.routers.health",
"app.routers.dashboard",
"app.routers.mappings",
"app.routers.sync",
"app.routers.validation",
"app.routers.articles",
"app.services.sqlite_service",
"app.services.scheduler_service",
"app.services.mapping_service",
"app.services.article_service",
"app.services.validation_service",
"app.services.import_service",
"app.services.sync_service",
"app.services.order_reader",
]
@pytest.mark.parametrize("module_name", MODULES)
def test_module_import(module_name):
"""Each app module should import without errors."""
__import__(module_name)
# -------------------------------------------------------
# Section 2: Route Tests via TestClient
# -------------------------------------------------------
# (path, expected_status_codes, is_known_oracle_failure)
GET_ROUTES = [
("/health", [200], False),
("/", [200, 500], False),
("/missing-skus", [200, 500], False),
("/mappings", [200, 500], False),
("/logs", [200, 500], False),
("/api/mappings", [200, 503], True),
("/api/mappings/export-csv", [200, 503], True),
("/api/mappings/csv-template", [200], False),
("/api/sync/status", [200], False),
("/api/sync/history", [200], False),
("/api/sync/schedule", [200], False),
("/api/validate/missing-skus", [200], False),
("/api/validate/missing-skus?page=1&per_page=10", [200], False),
("/api/sync/run/nonexistent/log", [200, 404], False),
("/api/articles/search?q=ab", [200, 503], True),
("/settings", [200, 500], False),
]
@pytest.fixture(scope="module")
def client():
"""Create a TestClient with lifespan for all route tests."""
from fastapi.testclient import TestClient
from app.main import app
with TestClient(app, raise_server_exceptions=False) as c:
yield c
@pytest.mark.parametrize(
"path,expected_codes,is_oracle_route",
GET_ROUTES,
ids=[p for p, _, _ in GET_ROUTES],
)
def test_route(client, path, expected_codes, is_oracle_route):
"""Each GET route should return an expected status code."""
resp = client.get(path)
assert resp.status_code in expected_codes, (
f"GET {path} returned {resp.status_code}, expected one of {expected_codes}. "
f"Body: {resp.text[:300]}"
)