from __future__ import annotations from pathlib import Path from uuid import uuid4 from services.control_api.persistence import create_engine_from_url, session_factory from services.control_api.persistence.models import ( AuditEventRow, EnvironmentRow, ProjectRow, WorkspaceRow, ) from tests.test_wp230_rbac import ManagementRole, RoleSession pytest_plugins = ("tests.test_wp230_rbac",) def test_resource_search_matches_connector_slug( role_sessions: dict[ManagementRole, RoleSession], ) -> None: response = role_sessions[ManagementRole.VIEWER].get( "/api/connectors", params={"q": "knowledge-default"} ) assert response.status_code == 200 body = response.json() assert [row["id"] for row in body["rows"]] == ["knowledge-default"] def test_project_scoped_connector_search_uses_bindings_and_environment( role_sessions: dict[ManagementRole, RoleSession], ) -> None: viewer = role_sessions[ManagementRole.VIEWER] response = viewer.get( "/api/connectors", params={ "project": "ludarium", "environment": "prod", "q": "knowledge-default", }, ) assert response.status_code == 200 rows = response.json()["rows"] assert len(rows) == 1 assert rows[0]["id"] == "knowledge-default" assert rows[0]["project"] == "ludarium" assert rows[0]["environment"] == "prod" assert not any("url" in key.casefold() for key in rows[0]) unbound_project = viewer.get( "/api/connectors", params={ "project": "mobilityops", "environment": "prod", "q": "knowledge-default", }, ) assert unbound_project.status_code == 200 assert unbound_project.json()["rows"] == [] def test_resource_search_matches_tool_canonical_name( role_sessions: dict[ManagementRole, RoleSession], ) -> None: response = role_sessions[ManagementRole.VIEWER].get( "/api/tools", params={"q": "deployment.project.status"} ) assert response.status_code == 200 body = response.json() assert [row["canonicalName"] for row in body["rows"]] == ["deployment.project.status"] def test_project_scoped_tool_search_uses_bindings_and_capability_packs( role_sessions: dict[ManagementRole, RoleSession], ) -> None: viewer = role_sessions[ManagementRole.VIEWER] ludarium_tools = viewer.get( "/api/tools", params={"project": "ludarium", "limit": 100}, ) assert ludarium_tools.status_code == 200 ludarium_body = ludarium_tools.json() ludarium_rows = ludarium_body["rows"] assert ludarium_body["page"]["total"] == len(ludarium_rows) ludarium_names = {row["canonicalName"] for row in ludarium_rows} assert "deployment.project.status" in ludarium_names assert "gitea.branch.delete" not in ludarium_names assert all(row["project"] == "ludarium" for row in ludarium_rows) assert all(row["environment"] == "prod" for row in ludarium_rows) unbound_connector = viewer.get( "/api/tools", params={ "project": "mobilityops", "environment": "prod", "q": "deployment.project.status", }, ) assert unbound_connector.status_code == 200 assert unbound_connector.json()["rows"] == [] environment_without_server = viewer.get( "/api/tools", params={ "project": "forgeflow", "environment": "dev", "q": "deployment.project.status", }, ) assert environment_without_server.status_code == 200 assert environment_without_server.json()["rows"] == [] def test_audit_search_matches_trace_id_without_exposing_search_to_viewers( role_sessions: dict[ManagementRole, RoleSession], db_path: Path ) -> None: trace_id = f"operator-search-{uuid4()}" engine = create_engine_from_url(f"sqlite:///{db_path}") sessions = session_factory(engine) with sessions() as session, session.begin(): workspace = session.query(WorkspaceRow).filter_by(slug="itworx").one() project = session.query(ProjectRow).filter_by(slug="ludarium").one() environment = ( session.query(EnvironmentRow).filter_by(project_id=project.id, name="prod").one() ) session.add( AuditEventRow( trace_id=trace_id, workspace_id=workspace.id, project_id=project.id, environment_id=environment.id, event_type="operator.search.fixture", tool_name="operator.search.fixture", policy_decision="must-not-be-a-search-field", outcome="success", ) ) engine.dispose() auditor = role_sessions[ManagementRole.AUDITOR] matching = auditor.get("/api/audit", params={"q": trace_id}) assert matching.status_code == 200 assert [row["traceId"] for row in matching.json()["rows"]] == [trace_id] excluded_field = auditor.get("/api/audit", params={"q": "must-not-be-a-search-field"}) assert excluded_field.status_code == 200 assert excluded_field.json()["rows"] == [] denied = role_sessions[ManagementRole.VIEWER].get("/api/audit", params={"q": trace_id}) assert denied.status_code == 403 def test_servers_list_virtual_server_identities_and_apply_project_environment_scope( role_sessions: dict[ManagementRole, RoleSession], ) -> None: viewer = role_sessions[ManagementRole.VIEWER] scoped = viewer.get( "/api/servers", params={"project": "forgeflow", "environment": "prod", "limit": 1}, ) assert scoped.status_code == 200 scoped_body = scoped.json() assert scoped_body["page"]["total"] == 1 assert scoped_body["rows"] == [ { "id": "forgeflow-prod-readonly", "name": "forgeflow-prod-readonly", "status": "unknown", "summary": "Catalog registered; live runtime health has not been observed.", "project": "forgeflow", "environment": "prod", "risk": None, "canonicalName": None, } ] wrong_environment = viewer.get( "/api/servers", params={"project": "forgeflow", "environment": "dev"} ) assert wrong_environment.status_code == 200 assert wrong_environment.json()["page"]["total"] == 0 assert wrong_environment.json()["rows"] == [] wrong_project = viewer.get( "/api/servers", params={"project": "mobilityops", "environment": "prod", "q": "forgeflow-prod-readonly"}, ) assert wrong_project.status_code == 200 assert wrong_project.json()["page"]["total"] == 0 assert wrong_project.json()["rows"] == []