Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions rust-core/verisim-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ pub struct HealthResponse {

/// Octad create/update request
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the documented nested Octad input contract

With deny_unknown_fields, schema-conforming POST/PUT payloads containing top-level document, vector, graph, or semantic objects now fail with 422 because those names are absent from OctadRequest. I checked the v1 OpenAPI reference to connectors/shared/json-schema/octad-input.json, the Rust client's nested OctadInput, and the repository smoke scripts, all of which send this nested shape; moreover, the Rust client serializes its unsupported optional fields even when they are None, so its create/update calls will be rejected universally. Accept the published nested fields here, or version and update the schema, clients, documentation, and scripts atomically rather than breaking the existing v1 contract.

Useful? React with 👍 / 👎.

pub struct OctadRequest {
/// Document title
pub title: Option<String>,
Expand Down Expand Up @@ -322,6 +323,16 @@ pub struct SpatialRequest {
}

impl OctadRequest {
/// Refuse payloads that would silently lose a supplied field.
fn validate(&self) -> Result<(), ApiError> {
if self.body.is_some() && self.title.is_none() {
return Err(ApiError::BadRequest(
"body requires title; omitting it would discard document content".to_string(),
));
}
Ok(())
}

/// Convert to OctadInput
fn to_octad_input(&self) -> OctadInput {
let mut input = OctadInput::default();
Expand Down Expand Up @@ -1061,6 +1072,7 @@ async fn create_octad_handler(
State(state): State<AppState>,
Json(request): Json<OctadRequest>,
) -> Result<(StatusCode, Json<OctadResponse>), ApiError> {
request.validate()?;
let input = request.to_octad_input();

let octad = state
Expand Down Expand Up @@ -1105,6 +1117,7 @@ async fn update_octad_handler(
) -> Result<Json<OctadResponse>, ApiError> {
validate_octad_id(&id)?;
let octad_id = OctadId::new(&id);
request.validate()?;
let input = request.to_octad_input();

// The pre-update modality set is the schema baseline: an update that
Expand Down Expand Up @@ -2692,6 +2705,48 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_reject_nested_schema_payload_instead_of_silently_dropping_it() {
let state = create_test_state().await;
let app = build_router(state);
let nested = serde_json::json!({
"document": {"title": "Keep", "body": "Must not be discarded"}
});

let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/octads")
.header("content-type", "application/json")
.body(Body::from(nested.to_string()))
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}

#[tokio::test]
async fn test_reject_body_without_title_instead_of_silently_dropping_it() {
let state = create_test_state().await;
let app = build_router(state);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/octads")
.header("content-type", "application/json")
.body(Body::from(r#"{"body":"A document without a title"}"#))
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn test_create_and_get_octad() {
let state = create_test_state().await;
Expand Down
Loading