diff --git a/api/openapi.json b/api/openapi.json index d0badfb..2ef0bc1 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -4374,16 +4374,6 @@ } } } - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/problem+json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetail" - } - } - } } } } diff --git a/docs/reference/api-design-v4_0_1.md b/docs/reference/api-design-v4_0_1.md index a03007d..b7f9f92 100644 --- a/docs/reference/api-design-v4_0_1.md +++ b/docs/reference/api-design-v4_0_1.md @@ -88,6 +88,7 @@ GET /api/v1/scans/{scan_id}/events - Job statuses: `queued`, `running`, `completed`, `partial`, `failed`, `canceled`. - Task statuses: `queued`, `running`, `completed`, `failed`, `skipped`, `blocked`, `canceled`. - A ScanResult belongs to one ScanTask, not directly to the ScanJob. +- A ScanResult carries an optional `trend`: movement against the previous result for the same test, as a `direction` (`improving`, `unchanged`, `declining`), a signed `delta`, and the id of the compared result. It tracks `grade` where the test is graded and total findings where it is not. Computed at read time; null on the first result for a test, or once the predecessor has been purged. - Specific causes use stable machine-readable `status_reason` values. Labels, descriptions, localization, and operator guidance stay code-owned. - A task timeout produces `failed` with a timeout `status_reason`. A job timeout terminates unfinished work; the job becomes `partial` when usable results exist, otherwise `failed`. - `POST /scans/{scan_id}/cancel` records cancellation intent and preserves scan history. `DELETE` is never used to stop execution. diff --git a/docs/reference/data-model-v4_0_1.md b/docs/reference/data-model-v4_0_1.md index 52360b5..7549e7e 100644 --- a/docs/reference/data-model-v4_0_1.md +++ b/docs/reference/data-model-v4_0_1.md @@ -531,6 +531,7 @@ Constraints: - `grade` is used only for Email, Web headers, and Web TLS tasks. - Non-graded tasks use severity counts or per-step verdicts in `summary`. - No cross-module composite score is stored. +- The API-level `ScanResult.trend` (`TrendDirection`: `improving`, `unchanged`, `declining`) is computed at read time from the previous comparable result and is not stored. No PostgreSQL enum carries it, like the computed verification status (§4.2). ### 8.2 `finding` diff --git a/src/nc3_testing_platform/domains/assets/examples.py b/src/nc3_testing_platform/domains/assets/examples.py index ef63657..d6c2c16 100644 --- a/src/nc3_testing_platform/domains/assets/examples.py +++ b/src/nc3_testing_platform/domains/assets/examples.py @@ -32,6 +32,15 @@ _T0 = datetime(2026, 6, 1, 8, 30, tzinfo=UTC) _T1 = datetime(2026, 7, 31, 9, 1, 12, tzinfo=UTC) +# The challenge is younger than the proof: its answer window must still be open at +# the _T1 recheck, or a `pending` sample would carry an unanswerable challenge. +# VERIFICATION_TOKEN_TTL is env-configurable, so the invariant is asserted rather +# than assumed — a TTL short enough to close the window fails at import. +_CHALLENGE_REQUESTED_AT = datetime(2026, 7, 28, 8, 30, tzinfo=UTC) +assert _CHALLENGE_REQUESTED_AT + VERIFICATION_TOKEN_TTL > _T1 + +_FEED_REVOKED_AT = datetime(2026, 7, 31, 10, 0, tzinfo=UTC) + _SUBDOMAIN_ASSET_ID = UUID("019ee1a3-0011-7a22-8b33-4c44d5e66f77") _CHALLENGE_ID = UUID("019ee1a3-1122-7b33-9c44-5d55e6f77a88") _FEED_ID = UUID("019ee1a3-2233-7c44-ad55-6e66f7a88b99") @@ -82,9 +91,9 @@ def sample_challenge(checked: bool = False) -> VerificationChallenge: requested_scope=VerificationScope.ZONE, record_name=verification_record_name("example.lu"), verification_token="verify-4f7a2c9e1b8d3056", - token_expires_at=_T0 + VERIFICATION_TOKEN_TTL, + token_expires_at=_CHALLENGE_REQUESTED_AT + VERIFICATION_TOKEN_TTL, requested_by_user_id=USER_ID, - requested_at=_T0, + requested_at=_CHALLENGE_REQUESTED_AT, last_recheck_at=_T1 if checked else None, failure_code="dns.txt_record_not_found" if checked else None, ) @@ -128,6 +137,13 @@ def sample_feed() -> AssetFeed: ) +def revoked_feed() -> AssetFeed: + """The feed after revocation, revoked after its last delivery.""" + feed = sample_feed() + feed.revoked_at = _FEED_REVOKED_AT + return feed + + def sample_feed_created() -> AssetFeedCreated: """The one response that carries the plaintext token.""" return AssetFeedCreated( diff --git a/src/nc3_testing_platform/domains/assets/router.py b/src/nc3_testing_platform/domains/assets/router.py index f944ba1..b8b0b93 100644 --- a/src/nc3_testing_platform/domains/assets/router.py +++ b/src/nc3_testing_platform/domains/assets/router.py @@ -237,9 +237,7 @@ async def revoke_asset_feed(asset_id: ResourceId, feed_id: ResourceId) -> AssetF A `POST` rather than a `DELETE`, because revocation is a recorded event and the lifecycle survives it. """ - feed = examples.sample_feed() - feed.revoked_at = feed.created_at - return feed + return examples.revoked_feed() @public_feed_router.get( diff --git a/src/nc3_testing_platform/domains/scans/examples.py b/src/nc3_testing_platform/domains/scans/examples.py index 3f2f014..ac3c417 100644 --- a/src/nc3_testing_platform/domains/scans/examples.py +++ b/src/nc3_testing_platform/domains/scans/examples.py @@ -199,6 +199,17 @@ def sample_job( ) +def canceled_job() -> ScanJob: + """The job once cancellation intent has taken effect. + + `canceled` appears in no other sample, and the cancel response is where a + client learns the terminal shape it must render. + """ + job = sample_job(status=ScanJobStatus.CANCELED) + job.status_reason = "scan.canceled_by_user" + return job + + def sample_job_detail() -> ScanJobDetail: """The job/task snapshot a live-progress client fetches before subscribing.""" return ScanJobDetail(**sample_job().model_dump(), tasks=sample_tasks()) diff --git a/src/nc3_testing_platform/domains/scans/router.py b/src/nc3_testing_platform/domains/scans/router.py index 940ffe3..021fb9c 100644 --- a/src/nc3_testing_platform/domains/scans/router.py +++ b/src/nc3_testing_platform/domains/scans/router.py @@ -275,7 +275,7 @@ async def cancel_scan(scan_id: ResourceId) -> ScanJob: safe interruption points; a canceled task cannot later produce an accepted successful result. """ - return examples.sample_job() + return examples.canceled_job() @router.post( diff --git a/src/nc3_testing_platform/domains/statements/router.py b/src/nc3_testing_platform/domains/statements/router.py index 890333a..32d9377 100644 --- a/src/nc3_testing_platform/domains/statements/router.py +++ b/src/nc3_testing_platform/domains/statements/router.py @@ -24,7 +24,6 @@ @router.get( "/statements", summary="List active statements", - responses=problem_responses(500), ) async def list_statements() -> list[Statement]: """Statements currently in force.