import asyncio from pathlib import Path import httpx2 from gewu.adapters.git_repository import GitCanonicalRepository from gewu.adapters.sqlite_identity import SQLiteIdentityStore from gewu.application.reading import CanonicalNodeReader from gewu.application.registry import LocalProjectRegistry from gewu.bootstrap.app import create_app from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus def test_project_registry_lifecycle_is_admin_only_and_durable(tmp_path: Path) -> None: async def exercise() -> None: identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3") admin_token = identities.provision( credential_id="project-admin", principal_id="owner-operator", roles=("admin",), ) viewer_token = identities.provision( credential_id="project-viewer", principal_id="research-reader", roles=("viewer",), ) mixed_agent_admin_token = identities.provision( credential_id="mixed-agent-admin", principal_id="agent-with-admin-role", roles=("admin", "agent"), ) repository = GitCanonicalRepository(tmp_path / "knowledge.git") registry = LocalProjectRegistry( tmp_path / "control" / "registry.json", repository=repository, index_directory=tmp_path / "publishing", authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock", ) app = create_app( identity_provider=identities, node_reader=CanonicalNodeReader(repository), project_registry=registry, ) project = ProjectRecord( project_id="new-research", title="New research", status=ProjectStatus.ACTIVE, description="An administrator-owned research lens.", owners=("owner-operator",), north_star="Investigate a new research area.", key_questions=("What is known?",), autonomy_policy_id="new-policy", ) policy = AutoAcceptPolicy( policy_id="new-policy", policy_version=1, project_id="new-research", require_supported_assessment=True, ) body = { "project": project.model_dump(mode="json"), "policy": policy.model_dump(mode="json"), } async with httpx2.AsyncClient( transport=httpx2.ASGITransport(app=app), base_url="http://test" ) as http: assert (await http.get("/v1/admin/projects")).status_code == 401 denied = await http.post( "/v1/admin/projects", json=body, headers={"Authorization": f"Bearer {viewer_token}"}, ) assert denied.status_code == 403 mixed_agent_denied = await http.post( "/v1/admin/projects", json=body, headers={"Authorization": f"Bearer {mixed_agent_admin_token}"}, ) assert mixed_agent_denied.status_code == 403 oversized = await http.post( "/v1/admin/projects", content=b" " * (64 * 1024 + 1), headers={ "Authorization": f"Bearer {admin_token}", "Content-Type": "application/json", }, ) assert oversized.status_code == 413 headers = {"Authorization": f"Bearer {admin_token}"} diagnostics = await http.get("/v1/diagnostics", headers=headers) assert diagnostics.status_code == 200 assert { "admin_list_projects", "project_create", "project_archive", }.issubset(diagnostics.json()["authorized_operations"]) created = await http.post("/v1/admin/projects", json=body, headers=headers) assert created.status_code == 201, created.text assert created.json()["project_id"] == "new-research" listing = await http.get("/v1/admin/projects", headers=headers) assert [item["project_id"] for item in listing.json()] == ["new-research"] duplicate = await http.post("/v1/admin/projects", json=body, headers=headers) assert duplicate.status_code == 409 archived = await http.post("/v1/admin/projects/new-research/archive", headers=headers) assert archived.status_code == 200 assert archived.json()["status"] == "archived" assert registry.records()[0].status == ProjectStatus.ARCHIVED session = identities.create_browser_session(admin_token) browser_project = project.model_copy( update={ "project_id": "browser-created", "title": "Browser created", "autonomy_policy_id": "browser-policy", } ) browser_policy = policy.model_copy( update={"policy_id": "browser-policy", "project_id": "browser-created"} ) browser_body = { "project": browser_project.model_dump(mode="json"), "policy": browser_policy.model_dump(mode="json"), } cookie = {"Cookie": f"gewu_session={session.session_token}"} without_csrf = await http.post( "/v1/admin/projects", json=browser_body, headers={**cookie, "Origin": "http://test"}, ) assert without_csrf.status_code == 403 wrong_origin = await http.post( "/v1/admin/projects", json=browser_body, headers={ **cookie, "Origin": "https://attacker.example", "X-GEWU-CSRF": session.csrf_token, }, ) assert wrong_origin.status_code == 403 browser_created = await http.post( "/v1/admin/projects", json=browser_body, headers={ **cookie, "Origin": "http://test", "X-GEWU-CSRF": session.csrf_token, }, ) assert browser_created.status_code == 201, browser_created.text asyncio.run(exercise())