Files
issue-core/tests/test_api_ingest.py
tegwick b605d970e3 feat: rename to issue-core and add task ingestion endpoint
Renames the package, distribution, CLI alias, Makefile targets, and
working directory from issue-facade to issue-core, signalling its
role as the authoritative task lifecycle manager for the Coulomb org
(peer to activity-core, rules-core, project-core).

Adds POST /issues/ ingestion endpoint for activity-core's IssueSink,
under a new optional [api] extra. The endpoint is served by `issue
serve`, authenticates via the ISSUE_CORE_API_KEY env var (Bearer or
X-API-Key header), and routes the TaskSpec payload to the configured
default backend with full traceability metadata embedded in
sync_metadata.

- T01: Python package issue_tracker -> issue_core, dir rename
- T02: registered in state hub under custodian domain
- T03: INTENT.md (what it is, what it isn't, how it fits)
- T04: SCOPE.md (in/out-of-scope, integration boundaries)
- T05: POST /issues/ via FastAPI + Uvicorn, 9 unit tests
- T06: docs/nats-task-ingestion.md design stub

Closes ISSC-WP-0001.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 05:16:27 +02:00

178 lines
5.4 KiB
Python

"""
Tests for POST /issues/ ingestion endpoint.
"""
import os
import tempfile
import uuid
from pathlib import Path
import pytest
pytest.importorskip("fastapi")
pytest.importorskip("httpx")
from fastapi.testclient import TestClient
from issue_core.api.app import create_app
API_KEY = "test-key-not-a-real-secret-only-for-pytest"
@pytest.fixture
def tmp_issue_store(monkeypatch, tmp_path):
"""Point cli.utils.get_config_dir at a tmp dir and the default backend at local."""
config_dir = tmp_path / "issue-core"
config_dir.mkdir()
monkeypatch.setattr(
"issue_core.cli.utils.get_config_dir", lambda: config_dir, raising=True
)
monkeypatch.setattr(
"issue_core.api.ingest.get_config_dir", lambda: config_dir, raising=True
)
db_path = str(config_dir / "issues.db")
monkeypatch.setattr(
"issue_core.cli.utils.load_backend_configs",
lambda: {"default": "local", "local": {"type": "local", "db_path": db_path}},
raising=True,
)
monkeypatch.setattr(
"issue_core.api.ingest.load_backend_configs",
lambda: {"default": "local", "local": {"type": "local", "db_path": db_path}},
raising=True,
)
return config_dir
@pytest.fixture
def client(monkeypatch, tmp_issue_store):
monkeypatch.setenv("ISSUE_CORE_API_KEY", API_KEY)
return TestClient(create_app())
@pytest.fixture
def valid_payload():
return {
"title": "Fix the parser",
"description": "Parser fails on multi-line input.",
"target_repo": "coulomb/parser",
"priority": "high",
"labels": ["bug"],
"due_in_days": 7,
"source_type": "rule",
"source_id": "rule:parse-failure",
"triggering_event_id": str(uuid.uuid4()),
"activity_definition_id": "ad:parser-monitor",
}
@pytest.mark.unit
def test_healthz(client):
response = client.get("/healthz")
assert response.status_code == 200
assert response.json()["status"] == "ok"
@pytest.mark.unit
def test_ingest_rejects_missing_api_key(client, valid_payload):
response = client.post("/issues/", json=valid_payload)
assert response.status_code == 401
@pytest.mark.unit
def test_ingest_rejects_wrong_api_key(client, valid_payload):
response = client.post(
"/issues/", json=valid_payload, headers={"Authorization": "Bearer wrong"}
)
assert response.status_code == 401
@pytest.mark.unit
def test_ingest_creates_issue_with_bearer(client, valid_payload):
response = client.post(
"/issues/",
json=valid_payload,
headers={"Authorization": f"Bearer {API_KEY}"},
)
assert response.status_code == 201, response.text
body = response.json()
assert body["backend"] == "sqlite"
assert body["issue_id"]
@pytest.mark.unit
def test_ingest_creates_issue_with_x_api_key(client, valid_payload):
response = client.post(
"/issues/",
json=valid_payload,
headers={"X-API-Key": API_KEY},
)
assert response.status_code == 201, response.text
@pytest.mark.unit
def test_ingest_rejects_invalid_payload(client):
bad = {"title": "no required fields"}
response = client.post(
"/issues/", json=bad, headers={"Authorization": f"Bearer {API_KEY}"}
)
assert response.status_code == 422
@pytest.mark.unit
def test_ingest_rejects_invalid_priority(client, valid_payload):
valid_payload["priority"] = "urgent"
response = client.post(
"/issues/",
json=valid_payload,
headers={"Authorization": f"Bearer {API_KEY}"},
)
assert response.status_code == 422
@pytest.mark.unit
def test_ingest_persists_traceability_metadata(client, valid_payload, tmp_issue_store):
"""Check that triggering_event_id, source_*, target_repo are stored on the issue."""
response = client.post(
"/issues/",
json=valid_payload,
headers={"Authorization": f"Bearer {API_KEY}"},
)
assert response.status_code == 201, response.text
issue_id = response.json()["issue_id"]
from issue_core.backends.local import LocalSQLiteBackend
backend = LocalSQLiteBackend()
backend.connect({"type": "local", "db_path": str(tmp_issue_store / "issues.db")})
try:
stored = backend.get_issue(issue_id)
assert stored is not None
ingestion = stored.sync_metadata.get("ingestion") or {}
assert ingestion["target_repo"] == valid_payload["target_repo"]
assert ingestion["source_type"] == valid_payload["source_type"]
assert ingestion["source_id"] == valid_payload["source_id"]
assert ingestion["triggering_event_id"] == valid_payload["triggering_event_id"]
assert ingestion["activity_definition_id"] == valid_payload["activity_definition_id"]
label_names = {label.name for label in stored.labels}
assert f"priority:{valid_payload['priority']}" in label_names
assert f"source:{valid_payload['source_type']}" in label_names
assert f"repo:{valid_payload['target_repo']}" in label_names
assert "bug" in label_names
finally:
backend.disconnect()
@pytest.mark.unit
def test_app_refuses_without_api_key_env(monkeypatch, tmp_issue_store, valid_payload):
monkeypatch.delenv("ISSUE_CORE_API_KEY", raising=False)
app = create_app()
client = TestClient(app)
response = client.post(
"/issues/", json=valid_payload, headers={"Authorization": "Bearer anything"}
)
assert response.status_code == 503
assert "ISSUE_CORE_API_KEY" in response.json()["detail"]