"""Gate F proof: a plugin built ONLY from docs/plugin-development-guide.md works. The guide's chapter 29.1 "Minimal Plugin" was transcribed verbatim into app/plugins/builtins/minimal_example/. This suite proves the resulting plugin is discovered, installable, activatable, and serves authenticated requests — without any guide corrections needed. """ from __future__ import annotations import pytest import pytest_asyncio from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from app.core.db import close_engine, reset_engine_for_testing from app.core.permission_registry import init_permission_registry from app.core.service_container import get_container from app.main import create_app from app.plugins.builtins.minimal_example.plugin import MinimalExamplePlugin from app.plugins.registry import reset_registry_for_testing from app.services.plugin_service import reset_plugin_service_for_testing from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users @pytest_asyncio.fixture async def gate_app(engine: AsyncEngine): reset_engine_for_testing(engine) app = create_app() registry = reset_registry_for_testing() registry.initialize(engine, app) init_permission_registry(active_plugin_names={"minimal_example"}) container = get_container() await container.initialize() registry.register_plugin(MinimalExamplePlugin()) reset_plugin_service_for_testing(registry) sf = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession) async with sf() as session: await registry.install(session, "minimal_example") await registry.activate(session, "minimal_example") await session.commit() yield app await close_engine() @pytest_asyncio.fixture async def gate_client(gate_app) -> AsyncClient: transport = ASGITransport(app=gate_app) async with AsyncClient(transport=transport, base_url="http://test") as c: yield c def test_manifest_matches_guide_example(): """The manifest is exactly the guide's 29.1 example.""" from app.plugins.builtins.minimal_example.plugin import MinimalExamplePlugin m = MinimalExamplePlugin().manifest assert m.name == "minimal_example" assert m.dependencies == [] assert [p.path for p in m.routes] == ["/api/v1/minimal-example"] assert m.permissions == ["minimal_example:read"] @pytest.mark.asyncio async def test_route_served_dynamically( gate_app, gate_client: AsyncClient, db_session: AsyncSession ): """Plugin routes are dispatched dynamically, not statically mounted. Gate F finding #4: /api/v1/minimal-example never appears in app.routes (plugin routes are resolved at request time against active plugins), yet the endpoint serves authenticated requests correctly. """ seed = await seed_tenant_and_users(db_session) assert seed is not None paths_before = [r.path for r in gate_app.routes if hasattr(r, "path")] assert "/api/v1/minimal-example" not in paths_before, ( "Plugin routes must NOT be statically mounted — they are dispatched dynamically" ) await login_client(gate_client, "admin@tenanta.com") resp = await gate_client.get("/api/v1/minimal-example", headers=ORIGIN_HEADER) assert resp.status_code == 200 assert resp.json() == {"items": []} @pytest.mark.asyncio async def test_endpoint_serves_authenticated_request( gate_client: AsyncClient, db_session: AsyncSession ): seed = await seed_tenant_and_users(db_session) assert seed is not None await login_client(gate_client, "admin@tenanta.com") resp = await gate_client.get("/api/v1/minimal-example", headers=ORIGIN_HEADER) assert resp.status_code == 200, resp.text assert resp.json() == {"items": []} @pytest.mark.asyncio async def test_endpoint_requires_permission_without_login( gate_client: AsyncClient, ): """Without login the permission guard rejects the request (401/403).""" resp = await gate_client.get("/api/v1/minimal-example", headers=ORIGIN_HEADER) assert resp.status_code in (401, 403)