Files
ss-tools/backend/tests/api/test_tools_mapper.py
busya a32ca0631b feat(037): capture, verification lifecycle, inheritance + close 036 stabilization
- Authoritative candidate capture with server-issued artifacts and raw-byte
  immutability hashing (source_response_hash server-owned)
- Closed-period lifecycle: request-hash bound approvals, persisted closure
  immutability violations, byte-for-byte catalog stability on reclosure
- Verification runs: persisted VerificationRun model + FK migration,
  publish gate (block_publish), scheduled observability runs (02:00 UTC)
- FR-013 baseline inheritance: prior_release_id migration, plan_inheritance/
  execute_inheritance classification and re-extraction, API endpoints
- Visual executor bound to release-deployment environment; caller mismatch
  rejected; visual SSIM/reconciliation modules
- Query execution decomposed: envelope/model/executor split, no direct SQL
- AgentRun approvals extracted to submodule; evidence adapter; _utils
- Dashboard testing service decomposed into 30+ modules (all <400 LOC)
- Five Feature-037 agent tools with permission guards (tools_037.py)
- API readiness endpoint; Alembic env/migrations; test fixture repos
- Specs 036/037 contracts, openapi.yaml, schema.json, tasks/traceability
  updated; semantic index rebuilt with 0 parse warnings
- Fix ADR-0003 parser ambiguity: remove [DEF🆔ADR] prose example
- Add axiom-mcp-agent-feedback.md: agent findings for MCP rework plan
- Tests: 298 service + 1464 API + 45 agent passing; ruff clean
2026-07-31 11:28:50 +03:00

186 lines
7.3 KiB
Python

# #region Test.Api.Tools.Mapper [C:3] [TYPE Module] [SEMANTICS test,tools,mapper,upload,xlsx]
# @BRIEF Tests for tools/mapper API routes — xlsx upload, validation.
# @RELATION BINDS_TO -> [Api.Tools.ToolsApiRoutes]
# @TEST_CONTRACT: File upload -> { path: str }
# @TEST_EDGE: invalid_extension -> 400 "Only .xlsx files are accepted"
# @TEST_EDGE: missing_columns -> 400 "Missing columns: 'column_name' and 'verbose_name'"
# @TEST_EDGE: empty_file -> 400 "XLSX file is empty"
# @TEST_EDGE: all_verbose_null -> 400 "at least one non-empty 'verbose_name' value"
# @TEST_EDGE: unauthorized -> 403 when permission denied
# @TEST_EDGE: success -> 200 with temp path
import io
import os
import uuid
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
os.environ.setdefault("DEV_MODE", "true")
import sys
from pathlib import Path
from unittest.mock import patch
import pandas as pd
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
def _make_xlsx_bytes(columns: dict[str, list]) -> bytes:
"""Create an in-memory xlsx file from column data."""
df = pd.DataFrame(columns)
buf = io.BytesIO()
df.to_excel(buf, index=False)
buf.seek(0)
return buf.read()
def _make_client(unauthorized: bool = False) -> TestClient:
"""Build a TestClient with tools router and overridden deps."""
from src.api.routes.tools import router
from src.dependencies import get_current_user
from src.schemas.auth import User, RoleSchema
app = FastAPI()
app.include_router(router)
if unauthorized:
# Return 403 when unauthorized flag is set
from fastapi import HTTPException
async def _raise_403():
raise HTTPException(status_code=403, detail="Forbidden")
app.dependency_overrides[get_current_user] = _raise_403
else:
# Admin user — passes all permission checks via the Admin role bypass
mock_user = User(
id="admin-1", username="admin", email="admin@x.com",
auth_source="LOCAL",
created_at=__import__("datetime").datetime.now(),
roles=[RoleSchema(id="r1", name="Admin", description="", is_admin=True, permissions=[])],
)
app.dependency_overrides[get_current_user] = lambda: mock_user
return TestClient(app)
class TestUploadXlsxMapping:
"""POST /api/tools/mapper/upload-xlsx"""
# #region Test.ToolsMapper.TestSuccessUploadValidXlsx [C:2] [TYPE Function]
# @BRIEF Valid xlsx with column_name and verbose_name returns 200 + path.
def test_success_upload_valid_xlsx(self):
client = _make_client()
xlsx_data = _make_xlsx_bytes({
"column_name": ["col_a", "col_b"],
"verbose_name": ["Column A", "Column B"],
})
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("test.xlsx", xlsx_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert resp.status_code == 200
data = resp.json()
assert "upload_id" in data
assert len(data["upload_id"]) == 32
# #endregion Test.ToolsMapper.TestSuccessUploadValidXlsx
# #region Test.ToolsMapper.TestInvalidExtension [C:2] [TYPE Function]
# @BRIEF Non-xlsx file extension returns 400.
def test_invalid_extension(self):
client = _make_client()
xlsx_data = _make_xlsx_bytes({
"column_name": ["col_a"],
"verbose_name": ["desc"],
})
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("test.csv", xlsx_data, "text/csv")},
)
assert resp.status_code == 400
assert "Only .xlsx files are accepted" in resp.json()["detail"]
# #endregion Test.ToolsMapper.TestInvalidExtension
# #region Test.ToolsMapper.TestMissingRequiredColumns [C:2] [TYPE Function]
# @BRIEF Xlsx missing column_name or verbose_name returns 400.
def test_missing_required_columns(self):
client = _make_client()
# Missing verbose_name column
xlsx_data = _make_xlsx_bytes({
"column_name": ["col_a"],
"other_col": ["val"],
})
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("test.xlsx", xlsx_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert resp.status_code == 400
assert "verbose_name" in resp.json()["detail"]
# #endregion Test.ToolsMapper.TestMissingRequiredColumns
# #region Test.ToolsMapper.TestEmptyXlsx [C:2] [TYPE Function]
# @BRIEF Empty xlsx (no data rows) returns 400.
def test_empty_xlsx(self):
client = _make_client()
df = pd.DataFrame({"column_name": pd.Series(dtype=str), "verbose_name": pd.Series(dtype=str)})
buf = io.BytesIO()
df.to_excel(buf, index=False)
buf.seek(0)
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("empty.xlsx", buf.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert resp.status_code == 400
assert "empty" in resp.json()["detail"].lower()
# #endregion Test.ToolsMapper.TestEmptyXlsx
# #region Test.ToolsMapper.TestAllVerboseNameNull [C:2] [TYPE Function]
# @BRIEF Xlsx with all-null verbose_name values returns 400.
def test_all_verbose_name_null(self):
client = _make_client()
xlsx_data = _make_xlsx_bytes({
"column_name": ["col_a", "col_b"],
"verbose_name": [None, None],
})
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("test.xlsx", xlsx_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert resp.status_code == 400
assert "verbose_name" in resp.json()["detail"].lower()
# #endregion Test.ToolsMapper.TestAllVerboseNameNull
# #region Test.ToolsMapper.TestUnauthorized [C:2] [TYPE Function]
# @BRIEF Missing permission returns 403.
def test_unauthorized(self):
client = _make_client(unauthorized=True)
xlsx_data = _make_xlsx_bytes({
"column_name": ["col_a"],
"verbose_name": ["desc"],
})
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("test.xlsx", xlsx_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert resp.status_code == 403
# #endregion Test.ToolsMapper.TestUnauthorized
# #region Test.ToolsMapper.TestOversizedUpload [C:2] [TYPE Function]
# @BRIEF Uploads larger than the configured limit return 413.
def test_oversized_upload(self):
client = _make_client()
oversized = b"x" * (10 * 1024 * 1024 + 1)
resp = client.post(
"/api/tools/mapper/upload-xlsx",
files={"file": ("large.xlsx", oversized, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
)
assert resp.status_code == 413
# #endregion Test.ToolsMapper.TestOversizedUpload
# #endregion TestUploadXlsxMapping
# #endregion Test.Api.Tools.Mapper