Merge branch 'wt/review-002-cov-deployment-knobs'
# Conflicts: # src/gateway/routes.rs
This commit is contained in:
@@ -1604,6 +1604,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_of_ref_resolves_each_item() {
|
||||
// The array branch of `resolve_refs_bounded` (the arm a coverage
|
||||
// pass initially mis-flagged): `$ref`s inside `items` resolve to
|
||||
// the component schema, not pass through verbatim.
|
||||
let spec = schema_test_spec(json!({
|
||||
"Tag": {"type": "string", "minLength": 1},
|
||||
"Tags": {"type": "array", "items": {"$ref": "#/components/schemas/Tag"}}
|
||||
}));
|
||||
let schema = spec
|
||||
.components
|
||||
.as_ref()
|
||||
.expect("test spec has schemas")
|
||||
.schemas
|
||||
.get("Tags")
|
||||
.expect("test schema present")
|
||||
.clone();
|
||||
let resolved = spec
|
||||
.resolve_refs_recursive(&schema)
|
||||
.expect("array-of-$ref is acyclic and must resolve");
|
||||
assert_eq!(resolved["type"], "array");
|
||||
assert_eq!(
|
||||
resolved["items"],
|
||||
serde_json::json!({"type": "string", "minLength": 1}),
|
||||
"the items $ref must expand to the Tag component schema"
|
||||
);
|
||||
let resolved_twice = spec
|
||||
.resolve_refs_recursive(&schema)
|
||||
.expect("second resolution hits the memo");
|
||||
assert_eq!(resolved, resolved_twice);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_via_all_of_errors() {
|
||||
let spec = schema_test_spec(json!({
|
||||
@@ -1839,6 +1871,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_style_with_explode_true_is_rejected() {
|
||||
let doc = r##"{
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "T", "version": "1"},
|
||||
"paths": {
|
||||
"/f": {"get": {
|
||||
"operationId": "f",
|
||||
"parameters": [{
|
||||
"name": "X-Id",
|
||||
"in": "header",
|
||||
"style": "simple",
|
||||
"explode": true,
|
||||
"schema": {"type": "string"}
|
||||
}],
|
||||
"responses": {"200": {"content": {"application/json": {"schema": {}}}}}
|
||||
}}
|
||||
}
|
||||
}"##;
|
||||
match OpenAPISpec::from_json(doc) {
|
||||
Err(AdapterError::SchemaParse { message }) => {
|
||||
assert!(
|
||||
message.contains("simple") && message.contains("explode"),
|
||||
"the error must name the simple+explode conflict: {message}"
|
||||
);
|
||||
assert!(message.contains("X-Id"), "message was: {message}");
|
||||
assert!(message.contains("OAI-06"), "message was: {message}");
|
||||
}
|
||||
Ok(_) => panic!("simple+explode=true has no wire meaning here; must fail loudly"),
|
||||
other => panic!("expected SchemaParse, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_style_and_explode_forms_still_import() {
|
||||
let doc = r##"{
|
||||
|
||||
@@ -1098,6 +1098,73 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn schema_tool_without_arguments_is_invalid_input() {
|
||||
let registry = full_registry_with_ops(vec![(
|
||||
"fs/readFile".to_string(),
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let result = invoke_tool(&gateway, "schema", None, None).await;
|
||||
assert_eq!(result.is_error, Some(true));
|
||||
let structured = result.structured_content.expect("structured error present");
|
||||
assert_eq!(
|
||||
structured.get("code"),
|
||||
Some(&Value::String("INVALID_INPUT".to_string()))
|
||||
);
|
||||
let message = structured
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.expect("message");
|
||||
assert!(
|
||||
message.contains("`name` is required"),
|
||||
"missing tool arguments must name the required field: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn batch_tool_without_calls_argument_is_invalid_input_without_dispatching() {
|
||||
let registry = full_registry_with_ops(vec![(
|
||||
"echo/run".to_string(),
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let none_args = invoke_tool(&gateway, "batch", None, None).await;
|
||||
assert_eq!(none_args.is_error, Some(true));
|
||||
let structured = none_args
|
||||
.structured_content
|
||||
.expect("structured error present");
|
||||
assert_eq!(
|
||||
structured.get("code"),
|
||||
Some(&Value::String("INVALID_INPUT".to_string()))
|
||||
);
|
||||
assert_eq!(structured.get("retryable"), Some(&Value::Bool(false)));
|
||||
let message = structured
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.expect("message");
|
||||
assert!(
|
||||
message.contains("`calls` is required"),
|
||||
"missing batch arguments must name the required field: {message}"
|
||||
);
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert("calls".to_string(), Value::String("nope".to_string()));
|
||||
let non_array = invoke_tool(&gateway, "batch", Some(args), None).await;
|
||||
assert_eq!(non_array.is_error, Some(true));
|
||||
let structured = non_array
|
||||
.structured_content
|
||||
.expect("structured error present");
|
||||
assert_eq!(
|
||||
structured.get("code"),
|
||||
Some(&Value::String("INVALID_INPUT".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn call_returns_structured_error_for_call_error() {
|
||||
let registry = full_registry_with_ops(vec![]);
|
||||
|
||||
@@ -2528,6 +2528,20 @@ mod tests {
|
||||
assert_eq!(body.get("code"), Some(&json!("INVALID_INPUT")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_first_line_invalid_json_returns_422_invalid_input() {
|
||||
let router = build_router(publish_registry(), unused_provider());
|
||||
let req = raw_request("POST", "/publish", b"not-json-at-all\n".to_vec());
|
||||
let (status, body) = send(router, req).await;
|
||||
assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
|
||||
assert_eq!(body.get("code"), Some(&json!("INVALID_INPUT")));
|
||||
let message = body.get("message").and_then(Value::as_str).unwrap_or("");
|
||||
assert!(
|
||||
message.contains("not valid JSON") || message.contains("JSON"),
|
||||
"the 422 must name the JSON parse failure: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_first_line_missing_chunk_returns_422_invalid_input() {
|
||||
let router = build_router(publish_registry(), unused_provider());
|
||||
|
||||
@@ -286,6 +286,26 @@ mod tests {
|
||||
assert_eq!(&bytes[..], b"<p>about</p>");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_site_decoy_directory_request_resolves_to_index_html() {
|
||||
let dir = tempfile_dir();
|
||||
tokio::fs::create_dir_all(dir.join("docs")).await.unwrap();
|
||||
tokio::fs::write(dir.join("docs").join("index.html"), "dir index")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let decoy = DecoyConfig::StaticSite { root: dir };
|
||||
let resp = send(decoy_router(decoy), "/docs").await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let ctype = resp
|
||||
.headers()
|
||||
.get(header::CONTENT_TYPE)
|
||||
.map(|v| v.to_str().unwrap().to_string());
|
||||
assert!(ctype.as_deref().unwrap_or("").starts_with("text/html"));
|
||||
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(&bytes[..], b"dir index");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_site_decoy_missing_file_returns_fake_404() {
|
||||
let dir = tempfile_dir();
|
||||
|
||||
Reference in New Issue
Block a user