feat: ingest HTTP 201/409 for event_id idempotency

- Validate UUID format; log created/duplicate/rejected

- Tests for 201, 409, 422; update agent-integration and work-plan

- Docs: neutral IDE wording (no product-specific editor names)
This commit is contained in:
PTah
2026-05-28 09:13:12 +10:00
parent 5cf76f492c
commit 2b76ae8497
11 changed files with 210 additions and 26 deletions
+82
View File
@@ -0,0 +1,82 @@
"""SQLite in-memory fixtures for API tests."""
import os
# Must be set before app.database imports create_engine
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SAC_BOOTSTRAP_API_KEY", "sac_test_key_for_pytest_only")
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import JSON, create_engine, event
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from app.auth.api_key import hash_api_key
from app.database import Base, get_db
from app.main import app as fastapi_app
import app.models # noqa: F401 — register all tables on Base.metadata
from app.models import ApiKey
TEST_API_KEY = os.environ["SAC_BOOTSTRAP_API_KEY"]
@event.listens_for(Base.metadata, "before_create")
def _sqlite_jsonb_as_json(metadata, connection, **_kwargs) -> None:
if connection.dialect.name != "sqlite":
return
for table in metadata.tables.values():
for column in table.columns:
if isinstance(column.type, JSONB):
column.type = JSON()
@pytest.fixture
def db_engine():
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture
def db_session(db_engine):
Session = sessionmaker(bind=db_engine, autocommit=False, autoflush=False)
session = Session()
session.add(
ApiKey(
name="test",
key_prefix=TEST_API_KEY[:12],
key_hash=hash_api_key(TEST_API_KEY),
is_active=True,
)
)
session.commit()
yield session
session.close()
@pytest.fixture
def client(db_session, monkeypatch):
monkeypatch.setattr("app.main.bootstrap_api_key", lambda: None)
def override_get_db():
try:
yield db_session
finally:
pass
fastapi_app.dependency_overrides[get_db] = override_get_db
with TestClient(fastapi_app) as c:
yield c
fastapi_app.dependency_overrides.clear()
@pytest.fixture
def auth_headers():
return {"Authorization": f"Bearer {TEST_API_KEY}"}
+63
View File
@@ -0,0 +1,63 @@
"""Ingest idempotency and HTTP status contract."""
import uuid
from app.services.schema_validate import validate_event_payload
VALID_EVENT = {
"schema_version": "1.0",
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"occurred_at": "2026-05-27T10:00:00+03:00",
"source": {"product": "ssh-monitor", "product_version": "1.2.3-SAC"},
"host": {"hostname": "test-host", "os_family": "linux"},
"category": "agent",
"type": "agent.test",
"severity": "info",
"title": "Test",
"summary": "pytest ingest",
}
def test_schema_rejects_non_uuid_event_id():
bad = {**VALID_EVENT, "event_id": "not-a-uuid"}
errors = validate_event_payload(bad)
assert errors
def test_schema_rejects_missing_event_id():
payload = {k: v for k, v in VALID_EVENT.items() if k != "event_id"}
errors = validate_event_payload(payload)
assert errors
def test_ingest_created_201(client, auth_headers):
event_id = str(uuid.uuid4())
payload = {**VALID_EVENT, "event_id": event_id}
r = client.post("/api/v1/events", json=payload, headers=auth_headers)
assert r.status_code == 201
data = r.json()
assert data["created"] is True
assert data["status"] == "created"
assert data["event_id"] == event_id
def test_ingest_duplicate_409(client, auth_headers):
event_id = str(uuid.uuid4())
payload = {**VALID_EVENT, "event_id": event_id}
assert client.post("/api/v1/events", json=payload, headers=auth_headers).status_code == 201
r = client.post("/api/v1/events", json=payload, headers=auth_headers)
assert r.status_code == 409
data = r.json()
assert data["created"] is False
assert data["status"] == "duplicate"
assert data["event_id"] == event_id
def test_ingest_invalid_payload_422(client, auth_headers):
r = client.post(
"/api/v1/events",
json={"event_id": "bad", "summary": "x"},
headers=auth_headers,
)
assert r.status_code == 422
assert "schema_errors" in r.json()["detail"]