feat(ttp): E.3.17 worker registration + scoped schemathesis suite

Wires decnet-ttp as a first-class worker:

* `decnet ttp` CLI command (master-only via MASTER_ONLY_COMMANDS)
* deploy/decnet-ttp.service.j2 systemd unit (After= identity / intel
  / reuse-correlator workers; ProtectHome=read-only since
  FilesystemRuleStore only reads ./rules/ttp/)
* deploy/decnet.target Wants= chain extended with decnet-ttp.service
* `ttp` was already in web/worker_registry.KNOWN_WORKERS

tests/api/test_schemathesis_ttp.py: TTP-routes-only schemathesis
suite, filtered via the OpenAPI tags=["TTP Tagging"] annotation
shared by the eight TTP routes. Reuses the live uvicorn subprocess
the wider test_schemathesis spawns; max_examples=400 keeps the
focused gate fast for E.3.13–E.3.16 iteration.

wiki-checkout/Service-Bus.md committed in its own repo: ttp.tagged
and ttp.rule.fired.<id> flipped from "reserved (TTP worker)" to
"decnet.ttp.worker" now that the worker publishes them.
This commit is contained in:
2026-05-01 21:26:46 -04:00
parent 07a609973b
commit 9a31d0e50c
6 changed files with 221 additions and 1 deletions

View File

@@ -0,0 +1,87 @@
"""Schemathesis contract tests scoped to the TTP Tagging API surface.
E.3.17 of ``development/TTP_TAGGING.md``. The full ``test_schemathesis``
suite fuzzes every endpoint with ``max_examples=3000`` — slow and
overkill when iterating on TTP-routes-only changes (E.3.13E.3.16).
This file filters by the OpenAPI ``tags=["TTP Tagging"]`` annotation
the eight TTP routes carry, runs against the same live uvicorn
subprocess the wider suite spins up, and applies the same check
battery so a 4xx-shape regression on a TTP route fails here without
waiting on the rest of the API.
Routes covered (all decorated ``tags=["TTP Tagging"]``):
* ``GET /api/v1/ttp/techniques``
* ``GET /api/v1/ttp/by-identity/{identity_uuid}``
* ``GET /api/v1/ttp/by-attacker/{attacker_uuid}``
* ``GET /api/v1/ttp/by-campaign/{campaign_uuid}``
* ``GET /api/v1/ttp/by-session/{session_id}``
* ``GET /api/v1/ttp/rules``
* ``POST /api/v1/ttp/rules/{rule_id}/state``
* ``DELETE /api/v1/ttp/rules/{rule_id}/state``
* ``GET /api/v1/ttp/export/navigator``
* ``GET /api/v1/ttp/export/navigator/identity/{identity_uuid}``
"""
from __future__ import annotations
import pytest
import schemathesis as st
from hypothesis import HealthCheck, Verbosity, settings
from tests.api.test_schemathesis import (
ALL_CHECKS,
AUTH_CHECKS,
LIVE_SERVER_URL,
before_call as _shared_before_call, # noqa: F401 (registers @st.hook)
)
# Reuse the schema fetched against the same uvicorn subprocess started
# by ``test_schemathesis``. Filtering by tag keeps the TTP suite a
# fast, focused contract gate without re-spinning the server.
TTP_SCHEMA = st.openapi.from_url(
f"{LIVE_SERVER_URL}/openapi.json",
).include(tag="TTP Tagging")
@pytest.mark.fuzz
@TTP_SCHEMA.parametrize()
@settings(
max_examples=400,
deadline=None,
verbosity=Verbosity.normal,
suppress_health_check=[
HealthCheck.filter_too_much,
HealthCheck.too_slow,
HealthCheck.data_too_large,
],
)
def test_ttp_schema_compliance(case):
"""Per-TTP-route schema compliance — valid + invalid inputs."""
case.call_and_validate(checks=ALL_CHECKS)
@pytest.mark.fuzz
@TTP_SCHEMA.parametrize()
@settings(
max_examples=120,
deadline=None,
verbosity=Verbosity.normal,
suppress_health_check=[
HealthCheck.filter_too_much,
HealthCheck.too_slow,
],
)
def test_ttp_auth_enforcement(case):
"""Every TTP route rejects requests without a Bearer token (401).
The mutation endpoints additionally require ``admin`` (server-side
``require_admin``); the authless probe doesn't distinguish 401 vs
403 here — the ``ignored_auth`` check just asserts that an absent
token never lands the request inside the handler with a usable
identity.
"""
case.headers = {
k: v for k, v in (case.headers or {}).items()
if k.lower() != "authorization"
}
case.call_and_validate(checks=AUTH_CHECKS)