"""Unit tests for GET /api/v1/scripts/count endpoint. Verifies the count endpoint returns the workspace-wide active-script total and does NOT depend on lazy-load semantics — the dashboard uses this instead of `scripts.length` to avoid underreporting. """ from __future__ import annotations from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock import pytest from sqlalchemy import func, select from backend.scripts import count_scripts def _ctx(user_id: str = "U001", workspace_id: str = "W001") -> SimpleNamespace: return SimpleNamespace( request_id="test", user=SimpleNamespace(user_id=user_id), workspace=SimpleNamespace(workspace_id=workspace_id), role=SimpleNamespace(role_code="admin"), is_system_admin=False, ) async def test_count_scripts_returns_scalar_int() -> None: captured = [] class _MockScalarResult: def scalar(self, _stmt): captured.append(_stmt) return 7 mock_session = MagicMock() mock_session.scalar = AsyncMock(side_effect=lambda stmt: (captured.append(stmt), 7)[1]) result = await count_scripts(context=_ctx(), session=mock_session) assert result["data"] == {"total": 7} assert result["meta"] == {} assert result["request_id"] == "test" # Exactly one COUNT(*) query issued. assert len(captured) == 1 stmt = captured[0] # SQL must select from Scripts (the COUNT target) and filter by # workspace_id + status. Bind params render as :workspace_id_1 etc. text = str(stmt).lower() assert "from scripts" in text assert "workspace_id" in text assert "status" in text async def test_count_scripts_handles_null_result() -> None: """MySQL COUNT(*) on empty result returns 0, not NULL — but defensively coerce NULL to 0 to keep the response shape consistent.""" mock_session = MagicMock() mock_session.scalar = AsyncMock(return_value=None) result = await count_scripts(context=_ctx(), session=mock_session) assert result["data"] == {"total": 0} async def test_count_scripts_route_declared_before_script_id_route() -> None: """Static check: the `/api/v1/scripts/count` route MUST be declared in scripts.py before `/api/v1/scripts/{script_id}/...`, otherwise FastAPI's declaration-order matching will interpret `count` as a script_id.""" from backend.scripts import count_scripts, get_script # Both callables exist (sanity). assert callable(count_scripts) assert callable(get_script)