Files
gomag-vending/api/test_integration.py
Marius Mutu 9c42187f02 feat: add FastAPI admin dashboard with sync orchestration and test suite
Replace Flask admin with FastAPI app (api/app/) featuring:
- Dashboard with stat cards, sync control, and history
- Mappings CRUD for ARTICOLE_TERTI with CSV import/export
- Article autocomplete from NOM_ARTICOLE
- SKU pre-validation before import
- Sync orchestration: read JSONs -> validate -> import -> log to SQLite
- APScheduler for periodic sync from UI
- File logging to logs/sync_comenzi_YYYYMMDD_HHMMSS.log
- Oracle pool None guard (503 vs 500 on unavailable)

Test suite:
- test_app_basic.py: 30 tests (imports + routes) without Oracle
- test_integration.py: 9 integration tests with Oracle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:35:16 +02:00

253 lines
9.9 KiB
Python

"""
Oracle Integration Tests for GoMag Import Manager
==================================================
Requires Oracle connectivity and valid .env configuration.
Usage:
cd /mnt/e/proiecte/vending/gomag
python api/test_integration.py
Note: Run from the project root so that relative paths in .env resolve correctly.
The .env file is read from the api/ directory.
"""
import os
import sys
# Set working directory to project root so relative paths in .env work
_script_dir = os.path.dirname(os.path.abspath(__file__))
_project_root = os.path.dirname(_script_dir)
os.chdir(_project_root)
# Load .env from api/ before importing app modules
from dotenv import load_dotenv
_env_path = os.path.join(_script_dir, ".env")
load_dotenv(_env_path, override=True)
# Add api/ to path so app package is importable
sys.path.insert(0, _script_dir)
from fastapi.testclient import TestClient
# Import the app (triggers lifespan on first TestClient use)
from app.main import app
results = []
def record(name: str, passed: bool, detail: str = ""):
status = "PASS" if passed else "FAIL"
msg = f"[{status}] {name}"
if detail:
msg += f" -- {detail}"
print(msg)
results.append(passed)
# ---------------------------------------------------------------------------
# Test A: GET /health — Oracle must show as connected
# ---------------------------------------------------------------------------
def test_health(client: TestClient):
test_name = "GET /health - Oracle connected"
try:
resp = client.get("/health")
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
oracle_status = body.get("oracle", "")
sqlite_status = body.get("sqlite", "")
assert oracle_status == "ok", f"oracle={oracle_status!r}"
assert sqlite_status == "ok", f"sqlite={sqlite_status!r}"
record(test_name, True, f"oracle={oracle_status}, sqlite={sqlite_status}")
except Exception as exc:
record(test_name, False, str(exc))
# ---------------------------------------------------------------------------
# Test B: Mappings CRUD cycle
# POST create -> GET list (verify present) -> PUT update -> DELETE -> verify
# ---------------------------------------------------------------------------
def test_mappings_crud(client: TestClient):
test_sku = "TEST_INTEG_SKU_001"
test_codmat = "TEST_CODMAT_001"
# -- CREATE --
try:
resp = client.post("/api/mappings", json={
"sku": test_sku,
"codmat": test_codmat,
"cantitate_roa": 2.5,
"procent_pret": 80.0
})
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
assert body.get("success") is True, f"create returned: {body}"
record("POST /api/mappings - create mapping", True,
f"sku={test_sku}, codmat={test_codmat}")
except Exception as exc:
record("POST /api/mappings - create mapping", False, str(exc))
# Skip the rest of CRUD if creation failed
return
# -- LIST (verify present) --
try:
resp = client.get("/api/mappings", params={"search": test_sku})
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
mappings = body.get("mappings", [])
found = any(
m["sku"] == test_sku and m["codmat"] == test_codmat
for m in mappings
)
assert found, f"mapping not found in list; got {mappings}"
record("GET /api/mappings - mapping visible after create", True,
f"total={body.get('total')}")
except Exception as exc:
record("GET /api/mappings - mapping visible after create", False, str(exc))
# -- UPDATE --
try:
resp = client.put(f"/api/mappings/{test_sku}/{test_codmat}", json={
"cantitate_roa": 3.0,
"procent_pret": 90.0
})
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
assert body.get("success") is True, f"update returned: {body}"
record("PUT /api/mappings/{sku}/{codmat} - update mapping", True,
"cantitate_roa=3.0, procent_pret=90.0")
except Exception as exc:
record("PUT /api/mappings/{sku}/{codmat} - update mapping", False, str(exc))
# -- DELETE (soft: sets activ=0) --
try:
resp = client.delete(f"/api/mappings/{test_sku}/{test_codmat}")
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
assert body.get("success") is True, f"delete returned: {body}"
record("DELETE /api/mappings/{sku}/{codmat} - soft delete", True)
except Exception as exc:
record("DELETE /api/mappings/{sku}/{codmat} - soft delete", False, str(exc))
# -- VERIFY: after soft-delete activ=0, listing without search filter should
# show it as activ=0 (it is still in DB). Search for it and confirm activ=0. --
try:
resp = client.get("/api/mappings", params={"search": test_sku})
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
mappings = body.get("mappings", [])
deleted = any(
m["sku"] == test_sku and m["codmat"] == test_codmat and m.get("activ") == 0
for m in mappings
)
assert deleted, (
f"expected activ=0 for deleted mapping, got: "
f"{[m for m in mappings if m['sku'] == test_sku]}"
)
record("GET /api/mappings - mapping has activ=0 after delete", True)
except Exception as exc:
record("GET /api/mappings - mapping has activ=0 after delete", False, str(exc))
# ---------------------------------------------------------------------------
# Test C: GET /api/articles/search?q=<term> — must return results
# ---------------------------------------------------------------------------
def test_articles_search(client: TestClient):
# Use a short generic term that should exist in most ROA databases
search_terms = ["01", "A", "PH"]
test_name = "GET /api/articles/search - returns results"
try:
found_results = False
last_body = {}
for term in search_terms:
resp = client.get("/api/articles/search", params={"q": term})
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
last_body = body
results_list = body.get("results", [])
if results_list:
found_results = True
record(test_name, True,
f"q={term!r} returned {len(results_list)} results; "
f"first={results_list[0].get('codmat')!r}")
break
if not found_results:
# Search returned empty — not necessarily a failure if DB is empty,
# but we flag it as a warning.
record(test_name, False,
f"all search terms returned empty; last response: {last_body}")
except Exception as exc:
record(test_name, False, str(exc))
# ---------------------------------------------------------------------------
# Test D: POST /api/validate/scan — triggers scan of JSON folder
# ---------------------------------------------------------------------------
def test_validate_scan(client: TestClient):
test_name = "POST /api/validate/scan - returns valid response"
try:
resp = client.post("/api/validate/scan")
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
# Must have at least these keys
for key in ("json_files", "orders", "skus"):
# "orders" may be "total_orders" if orders exist; "orders" key only
# present in the "No orders found" path.
pass
# Accept both shapes: no-orders path has "orders" key, full path has "total_orders"
has_shape = "json_files" in body and ("orders" in body or "total_orders" in body)
assert has_shape, f"unexpected response shape: {body}"
record(test_name, True, f"json_files={body.get('json_files')}, "
f"orders={body.get('total_orders', body.get('orders'))}")
except Exception as exc:
record(test_name, False, str(exc))
# ---------------------------------------------------------------------------
# Test E: GET /api/sync/history — must return a list structure
# ---------------------------------------------------------------------------
def test_sync_history(client: TestClient):
test_name = "GET /api/sync/history - returns list structure"
try:
resp = client.get("/api/sync/history")
assert resp.status_code == 200, f"HTTP {resp.status_code}"
body = resp.json()
assert "runs" in body, f"missing 'runs' key; got keys: {list(body.keys())}"
assert isinstance(body["runs"], list), f"'runs' is not a list: {type(body['runs'])}"
assert "total" in body, f"missing 'total' key"
record(test_name, True,
f"total={body.get('total')}, page={body.get('page')}, pages={body.get('pages')}")
except Exception as exc:
record(test_name, False, str(exc))
# ---------------------------------------------------------------------------
# Main runner
# ---------------------------------------------------------------------------
def main():
print("=" * 60)
print("GoMag Import Manager - Oracle Integration Tests")
print(f"Env file: {_env_path}")
print(f"Oracle DSN: {os.environ.get('ORACLE_DSN', '(not set)')}")
print("=" * 60)
with TestClient(app) as client:
test_health(client)
test_mappings_crud(client)
test_articles_search(client)
test_validate_scan(client)
test_sync_history(client)
passed = sum(results)
total = len(results)
print("=" * 60)
print(f"Summary: {passed}/{total} tests passed")
if passed < total:
print("Some tests FAILED — review output above for details.")
sys.exit(1)
else:
print("All tests PASSED.")
if __name__ == "__main__":
main()