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>
253 lines
9.9 KiB
Python
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()
|