use std::time::Duration;
use tonic::{Request, Response, Status};
use crate::proto::{
RegisterSchemaRequest, RegisterSchemaResponse, UnregisterSchemaRequest,
UnregisterSchemaResponse, ValidateRequest, ValidateResponse, ValidateWithRequest,
ValidationError,
xsd_validator_client::XsdValidatorClient,
xsd_validator_server::{XsdValidator, XsdValidatorServer},
};
use crate::xsd_bridge::xsd_bridge_client;
const NEAR_CAP_ERROR_SIZE: usize = 5 * 1024 * 1024;
fn near_cap_error_text() -> String {
(0..NEAR_CAP_ERROR_SIZE)
.map(|i| char::from(b'a' + (i % 25) as u8))
.collect()
}
#[derive(Clone)]
struct MockXsdBridge {
validation_error: Option<String>,
}
#[tonic::async_trait]
impl XsdValidator for MockXsdBridge {
async fn register_schema(
&self,
_request: Request<RegisterSchemaRequest>,
) -> Result<Response<RegisterSchemaResponse>, Status> {
Err(Status::unimplemented("register_schema not exercised here"))
}
async fn validate_with(
&self,
_request: Request<ValidateWithRequest>,
) -> Result<Response<ValidateResponse>, Status> {
let message = self
.validation_error
.clone()
.ok_or_else(|| Status::internal("no prepared validation error"))?;
Ok(Response::new(ValidateResponse {
valid: false,
errors: vec![ValidationError {
message,
line: 1,
column: 1,
severity: String::new(),
}],
error: None,
}))
}
async fn unregister_schema(
&self,
_request: Request<UnregisterSchemaRequest>,
) -> Result<Response<UnregisterSchemaResponse>, Status> {
Err(Status::unimplemented(
"unregister_schema not exercised here",
))
}
async fn validate(
&self,
_request: Request<ValidateRequest>,
) -> Result<Response<ValidateResponse>, Status> {
Err(Status::unimplemented("validate not exercised here"))
}
}
async fn spawn_mock_bridge(error_text: String) -> std::io::Result<u16> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let port = listener.local_addr()?.port();
let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
tokio::spawn(async move {
let _ = tonic::transport::Server::builder()
.add_service(XsdValidatorServer::new(MockXsdBridge {
validation_error: Some(error_text),
}))
.serve_with_incoming(incoming)
.await;
});
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(port)
}
#[tokio::test]
async fn near_cap_validation_error_decodes_end_to_end() {
let error_text = near_cap_error_text();
let port = spawn_mock_bridge(error_text.clone())
.await
.expect("spawn mock bridge");
let endpoint = tonic::transport::Endpoint::from_shared(format!("http://127.0.0.1:{port}"))
.expect("valid endpoint uri");
let raw_channel = endpoint.connect().await.expect("connect to mock bridge");
let mut raw_client = XsdValidatorClient::new(raw_channel);
let _raw_err = raw_client
.validate_with(Request::new(ValidateWithRequest {
schema_id: "xsd-near-cap".to_string(),
document: b"<doc/>".to_vec(),
}))
.await
.expect_err("raw client must hit the 4 MiB default decode limit");
let channel = endpoint.connect().await.expect("connect to mock bridge");
let mut client = xsd_bridge_client(channel);
let response = client
.validate_with(Request::new(ValidateWithRequest {
schema_id: "xsd-near-cap".to_string(),
document: b"<doc/>".to_vec(),
}))
.await
.expect("validate_with decodes through the production helper");
let delivered = response.into_inner();
assert_eq!(delivered.errors.len(), 1);
assert_eq!(delivered.errors[0].message.len(), NEAR_CAP_ERROR_SIZE);
assert_eq!(
delivered.errors[0].message, error_text,
"validation error text must arrive byte-intact below the 17 MiB decode limit"
);
}
#[test]
fn xsd_bridge_decode_limit_above_cap() {
assert!(
crate::xsd_bridge::xsd_bridge_decode_limit() >= 17 * 1024 * 1024,
"xsd decode limit must stay at or above the 16 MiB bridge cap + 1 MiB headroom"
);
}