refactor: single refusal shape with openapi contract test
This commit is contained in:
93
crates/adapters/http-axum/tests/openapi_contract_test.rs
Normal file
93
crates/adapters/http-axum/tests/openapi_contract_test.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
fn spec() -> serde_json::Value {
|
||||
serde_json::to_value(http_axum::openapi::spec()).unwrap()
|
||||
}
|
||||
|
||||
fn operations(spec: &serde_json::Value) -> Vec<(String, String, serde_json::Value)> {
|
||||
spec["paths"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.flat_map(|(path, methods)| {
|
||||
methods
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(move |(verb, op)| (verb.to_uppercase(), path.clone(), op.clone()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_that_can_succeed_with_a_body_declares_its_shape() {
|
||||
let spec = spec();
|
||||
let mut untyped = Vec::new();
|
||||
|
||||
for (verb, path, op) in operations(&spec) {
|
||||
let responses = op["responses"].as_object().unwrap();
|
||||
let typed = responses.iter().any(|(code, r)| {
|
||||
code.starts_with('2') && r["content"]["application/json"]["schema"].is_object()
|
||||
});
|
||||
let bodyless = responses
|
||||
.iter()
|
||||
.any(|(code, r)| code.starts_with('2') && r.get("content").is_none());
|
||||
let binary = responses.iter().any(|(code, r)| {
|
||||
code.starts_with('2')
|
||||
&& r["content"]
|
||||
.as_object()
|
||||
.is_some_and(|c| !c.contains_key("application/json"))
|
||||
});
|
||||
if !typed && !bodyless && !binary {
|
||||
untyped.push(format!("{verb} {path}"));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(untyped.is_empty(), "2xx without a schema: {untyped:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_operation_documents_a_refusal() {
|
||||
let spec = spec();
|
||||
let silent: Vec<_> = operations(&spec)
|
||||
.into_iter()
|
||||
.filter(|(_, _, op)| {
|
||||
!op["responses"]
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.any(|c| c.starts_with('4'))
|
||||
})
|
||||
.map(|(v, p, _)| format!("{v} {p}"))
|
||||
.collect();
|
||||
assert!(silent.is_empty(), "no 4xx documented: {silent:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_authenticated_operation_documents_401() {
|
||||
let spec = spec();
|
||||
let silent: Vec<_> = operations(&spec)
|
||||
.into_iter()
|
||||
.filter(|(_, _, op)| op.get("security").is_some() && op["responses"].get("401").is_none())
|
||||
.map(|(v, p, _)| format!("{v} {p}"))
|
||||
.collect();
|
||||
assert!(silent.is_empty(), "no 401 documented: {silent:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_refusal_is_the_one_error_shape() {
|
||||
let spec = spec();
|
||||
let mut odd = Vec::new();
|
||||
for (verb, path, op) in operations(&spec) {
|
||||
for (code, response) in op["responses"].as_object().unwrap() {
|
||||
if !code.starts_with('4') && !code.starts_with('5') {
|
||||
continue;
|
||||
}
|
||||
let named = response["content"]["application/json"]["schema"]["$ref"]
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if named != "#/components/schemas/ErrorResponse" {
|
||||
odd.push(format!("{verb} {path} -> {code}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(odd.is_empty(), "refusals not ErrorResponse: {odd:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user