"""Publish API (CANP-WP-0006 T04).""" from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from canned_prompts_service.api import create_app from canned_prompts_service.db import make_engine, make_session_factory from canned_prompts_service.models import NamespaceClaim from canned_prompts_service.settings import Settings REPO = Path(__file__).resolve().parents[2] TOKEN = "test-token" AUTH = {"Authorization": f"Bearer {TOKEN}"} def package_payload(name: str, registry: str = "local") -> dict: """Built from the archive shape, so an archive round-trips into a publish.""" source_dir = REPO / "examples" / name import canned_prompts as cp manifest = cp.validate_package(source_dir) files = { rel: {"text": (source_dir / rel).read_text(encoding="utf-8")} for rel in sorted(cp.package_members(source_dir, manifest)) } return {"registry": registry, "source": f"examples/{name}", "files": files} @pytest.fixture() def writable(db_url: str) -> TestClient: settings = Settings(database_url=db_url, publish_token=TOKEN) return TestClient(create_app(settings, make_engine(db_url))) def test_unconfigured_service_refuses_writes(client: TestClient) -> None: """No publisher identity means the service cannot tell who is calling.""" response = client.post("/packages", json=package_payload("house-style")) assert response.status_code == 503 assert "publishing is not configured" in response.json()["detail"] def test_publish_requires_a_token(writable: TestClient) -> None: response = writable.post("/packages", json=package_payload("house-style")) assert response.status_code == 401 def test_wrong_token_is_refused(writable: TestClient) -> None: response = writable.post( "/packages", json=package_payload("house-style"), headers={"Authorization": "Bearer nope"}, ) assert response.status_code == 403 def test_publish_then_read_back(writable: TestClient) -> None: response = writable.post("/packages", json=package_payload("house-style"), headers=AUTH) assert response.status_code == 201 assert response.json()["reference"] == "local:practice/house-style@0.1.1" body = writable.get("/packages/local:practice/house-style@0.1.1").json() assert body["type"] == "fragment" assert writable.get("/index").json()["count"] == 1 def test_archive_round_trips_into_a_publish(writable: TestClient) -> None: """What GET /archives returns is exactly what POST /packages accepts.""" writable.post("/packages", json=package_payload("house-style"), headers=AUTH) archive = writable.get("/archives/practice/house-style@0.1.1").json() republished = writable.post( "/packages", json={"registry": "mirror", "source": "round-trip", "files": archive["files"]}, headers=AUTH, ) assert republished.status_code == 201 assert republished.json()["content_digest"] == archive["content_digest"] def test_identical_republish_is_accepted(writable: TestClient) -> None: payload = package_payload("house-style") assert writable.post("/packages", json=payload, headers=AUTH).status_code == 201 assert writable.post("/packages", json=payload, headers=AUTH).status_code == 201 def test_different_content_under_the_same_version_is_a_conflict(writable: TestClient) -> None: """§ 17: a published id@version is immutable within a registry.""" payload = package_payload("house-style") writable.post("/packages", json=payload, headers=AUTH) payload["files"]["prompt.md"]["text"] += "\nsomething different\n" response = writable.post("/packages", json=payload, headers=AUTH) assert response.status_code == 409 assert "already exists with different content" in response.json()["detail"] def test_invalid_package_is_rejected(writable: TestClient) -> None: payload = package_payload("house-style") payload["files"]["prompt.yaml"]["text"] = "format: canned-prompt/v9.9\nid: x/y\n" response = writable.post("/packages", json=payload, headers=AUTH) assert response.status_code == 400 def test_path_traversal_is_refused(writable: TestClient) -> None: payload = package_payload("house-style") payload["files"]["../escape.txt"] = {"text": "no"} assert writable.post("/packages", json=payload, headers=AUTH).status_code == 400 def test_closed_namespace_refuses_a_publisher_it_cannot_attribute( db_url: str, session: Session ) -> None: """§ 20.1 enforced rather than advised — as far as identity allows.""" session.add( NamespaceClaim( tenant="default", registry="local", namespace="practice", policy="closed", owner="someone-else", ) ) session.commit() client = TestClient( create_app(Settings(database_url=db_url, publish_token=TOKEN), make_engine(db_url)) ) response = client.post("/packages", json=package_payload("house-style"), headers=AUTH) assert response.status_code == 403 assert "is closed" in response.json()["detail"] def test_open_namespace_publishes(db_url: str, session: Session) -> None: session.add( NamespaceClaim( tenant="default", registry="local", namespace="practice", policy="open" ) ) session.commit() client = TestClient( create_app(Settings(database_url=db_url, publish_token=TOKEN), make_engine(db_url)) ) assert client.post( "/packages", json=package_payload("house-style"), headers=AUTH ).status_code == 201