Codurile ROA contin legitim '/' si '"' (ex. RCR1/4"). Rutele FastAPI
foloseau parametri de cale (/api/mappings/{sku}/{codmat}...); ASGI decodeaza
%2F -> '/' INAINTE de routing, deci o cheie cu slash devine doua segmente si
Starlette nu mai potriveste ruta -> 404 la edit/delete/restore. Cand ambii
parametri au slash, un convertor {path} nu rezolva (split ambiguu).
Solutie: sku/codmat trec din cale in query string. %2F/%22 din query se
decodeaza ca parte a valorii, fara sa atinga routing-ul de cale.
- backend: 4 rute statice + Query(...) (update/edit/delete/restore);
semnaturile mapping_service raman neschimbate
- frontend: helper mapQS(); toate cele 8 apeluri path -> query; cache-bust ?v=19
- tests: regresie ci-layer (chei-sentinel cu slash, !=404, toate 4 rutele);
test_integration update/delete convertite la query; e2e delete RCR1/4"
(slash+ghilimea) prin modalul UI, skip cand Oracle lipseste
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
137 lines
4.6 KiB
Python
137 lines
4.6 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]}"
|
|
)
|
|
|
|
|
|
def test_mappings_slash_in_keys_routes_ok(client):
|
|
"""Regression: slash in BOTH sku and codmat used to 404 (path-param routes,
|
|
ASGI decoded %2F before routing). Now sku/codmat are query params, so the
|
|
route resolves regardless of slashes/quotes. Sentinel keys with a slash can
|
|
match no real row, so even if Oracle were up the UPDATE/WHERE is a no-op —
|
|
here pool is None so the handler returns 200 success:False without mutation.
|
|
Asserting != 404 proves the route resolved."""
|
|
sku, codmat = '__TEST_SKU/X"', '__TEST_CODMAT/Y'
|
|
calls = [
|
|
client.delete("/api/mappings/delete", params={"sku": sku, "codmat": codmat}),
|
|
client.put("/api/mappings/update", params={"sku": sku, "codmat": codmat},
|
|
json={"cantitate_roa": 1}),
|
|
client.put("/api/mappings/edit", params={"sku": sku, "codmat": codmat},
|
|
json={"new_sku": sku, "new_codmat": codmat, "cantitate_roa": 1}),
|
|
client.post("/api/mappings/restore", params={"sku": sku, "codmat": codmat}),
|
|
]
|
|
for resp in calls:
|
|
assert resp.status_code != 404, (
|
|
f"route did not resolve: {resp.status_code} {resp.url}"
|
|
)
|