Files
GEWU/tests/api/test_project_admin.py
T
2026-10-14 19:21:35 +08:00

154 lines
6.3 KiB
Python

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())