# #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