test: pin AppError -> HTTP status mapping (incl 501 and 429 Retry-After)
Table-driven test over every AppError variant's into_response().status(), closing the unexercised 501 NotImplemented path and asserting a deterministic 429 Retry-After header (audit GAP B). Test-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -207,4 +207,68 @@ mod tests {
|
|||||||
"text_search_not_yet_supported"
|
"text_search_not_yet_supported"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn status_mapping_is_stable() {
|
||||||
|
// Pin the code -> HTTP status mapping so a refactor of into_response
|
||||||
|
// can't silently change a status (e.g. 501 -> 500). The 501 NotImplemented
|
||||||
|
// path and a deterministic 429 Retry-After were previously unexercised
|
||||||
|
// (audit GAP B).
|
||||||
|
let cases: Vec<(AppError, StatusCode)> = vec![
|
||||||
|
(AppError::NotFound, StatusCode::NOT_FOUND),
|
||||||
|
(AppError::InvalidInput("x".into()), StatusCode::BAD_REQUEST),
|
||||||
|
(AppError::Unauthenticated, StatusCode::UNAUTHORIZED),
|
||||||
|
(AppError::Forbidden, StatusCode::FORBIDDEN),
|
||||||
|
(AppError::Conflict("x".into()), StatusCode::CONFLICT),
|
||||||
|
(AppError::PayloadTooLarge("x".into()), StatusCode::PAYLOAD_TOO_LARGE),
|
||||||
|
(
|
||||||
|
AppError::UnsupportedMediaType("x".into()),
|
||||||
|
StatusCode::UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AppError::ServiceUnavailable("x".into()),
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AppError::ValidationFailed {
|
||||||
|
message: "x".into(),
|
||||||
|
details: json!({}),
|
||||||
|
},
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AppError::NotImplemented {
|
||||||
|
code: "c",
|
||||||
|
message: "m",
|
||||||
|
},
|
||||||
|
StatusCode::NOT_IMPLEMENTED,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AppError::TooManyRequests {
|
||||||
|
retry_after_secs: None,
|
||||||
|
},
|
||||||
|
StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
AppError::Other(anyhow::anyhow!("boom")),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (err, want) in cases {
|
||||||
|
let code = err.code();
|
||||||
|
assert_eq!(err.into_response().status(), want, "status for code {code}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 429 with retry_after carries the Retry-After header deterministically
|
||||||
|
// (without needing a live rate limiter).
|
||||||
|
let resp = AppError::TooManyRequests {
|
||||||
|
retry_after_secs: Some(7),
|
||||||
|
}
|
||||||
|
.into_response();
|
||||||
|
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(axum::http::header::RETRY_AFTER).unwrap(),
|
||||||
|
"7"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user