Skip to main content
Glama
validations.rs26.7 kB
use std::time::Duration; use dal::{ AttributeValue, ChangeSet, Component, DalContext, attribute::{ path::AttributePath, value::subscription::ValueSubscription, }, workspace_snapshot::{ content_address::ContentAddressDiscriminants, edge_weight::EdgeWeightKindDiscriminants, node_weight::reason_node_weight::Reason, }, }; use dal_test::{ Result, expected::ExpectSchemaVariant, helpers::{ ChangeSetTestHelpers, PropEditorTestView, attribute::value, component, component::find_management_prototype, create_component_for_default_schema_name_in_default_view, extract_value_and_validation, }, test, }; use pretty_assertions_sorted::assert_eq; use serde_json::json; #[test] async fn validation_format_errors(ctx: &mut DalContext) -> Result<()> { let component = create_component_for_default_schema_name_in_default_view(ctx, "BadValidations", "bad") .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let bad_json_path = &["root", "domain", "bad_validation_json"]; let prop_view = PropEditorTestView::for_component_id(ctx, component.id()) .await? .get_value(bad_json_path)?; assert_eq!( json!({ "value": null, "validation": { "status": "Error", "message": "UserCodeException: Invalid JSON format", } }), extract_value_and_validation(prop_view)? ); let bad_format_path = &["root", "domain", "bad_validation_format"]; let prop_view = PropEditorTestView::for_component_id(ctx, component.id()) .await? .get_value(bad_format_path)?; assert_eq!( json!({ "value": null, "validation": { "status": "Error", "message": "UserCodeException: validationFormat 5 is wrong: ValidationError: \"value\" must be of type object", } }), extract_value_and_validation(prop_view)? ); Ok(()) } #[test] async fn prop_editor_validation(ctx: &mut DalContext) -> Result<()> { let component = create_component_for_default_schema_name_in_default_view(ctx, "pirate", "Robinson Crusoe") .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_path = &["root", "domain", "working_eyes"]; let av_id = component .attribute_values_for_prop(ctx, prop_path) .await? .pop() .expect("there should only be one value id"); let prop_view = PropEditorTestView::for_component_id(ctx, component.id()) .await? .get_value(prop_path)?; assert_eq!( json!({ "value": null, "validation": { "status": "Failure", "message": "\"value\" is required", } }), extract_value_and_validation(prop_view)? ); AttributeValue::update(ctx, av_id, Some(json!(1))).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_view = PropEditorTestView::for_component_id(ctx, component.id()) .await? .get_value(prop_path)?; assert_eq!( json!({ "value": 1, "validation": { "status": "Success", "message": null, } }), extract_value_and_validation(prop_view)? ); AttributeValue::update(ctx, av_id, Some(json!(3))).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_view = PropEditorTestView::for_component_id(ctx, component.id()) .await? .get_value(prop_path)?; assert_eq!( json!({ "value": 3, "validation": { "status": "Failure", "message": "\"value\" must be less than or equal to 2", } }), extract_value_and_validation(prop_view)? ); Ok(()) } #[test] async fn validation_pre_post_mgmt_func(ctx: &mut DalContext) -> Result<()> { let component = create_component_for_default_schema_name_in_default_view(ctx, "ValidatedOutput", "Output") .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // check default post create - should be one failure because a_number is required let component_in_list = dal_materialized_views::component::assemble(ctx.clone(), component.id()).await?; assert!(component_in_list.qualification_totals.failed == 1); // now let's run the mgmt func that should fix this one let management_prototype = find_management_prototype(ctx, component.id(), "Good import validated output").await?; ChangeSetTestHelpers::enqueue_management_func_job( ctx, management_prototype.id(), component.id(), None, ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for dvu ChangeSet::wait_for_dvu(ctx, false).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Check that the validation is now passing as the mgmt func wrote a valid value // loop to wait for the validation job to finish let seconds = 10; let mut did_pass = false; for _ in 0..(seconds * 10) { ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let component_in_list = dal_materialized_views::component::assemble(ctx.clone(), component.id()).await?; if component_in_list.qualification_totals.failed == 0 { did_pass = true; break; } tokio::time::sleep(Duration::from_millis(100)).await; } if !did_pass { panic!( "Validation job should have finished and set the value correctly, but it did not. Must investigate!" ); } // now let's run the bad import mgmt func (that sets a non-valid value) let bad_management_prototype = find_management_prototype(ctx, component.id(), "Bad import validated output").await?; ChangeSetTestHelpers::enqueue_management_func_job( ctx, bad_management_prototype.id(), component.id(), None, ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for dvu ChangeSet::wait_for_dvu(ctx, false).await?; // now check that the validation is failing again // loop to wait for the validation job to finish let seconds = 10; let mut did_pass = false; for _ in 0..(seconds * 10) { ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let component_in_list = dal_materialized_views::component::assemble(ctx.clone(), component.id()).await?; if component_in_list.qualification_totals.failed == 1 { did_pass = true; break; } tokio::time::sleep(Duration::from_millis(100)).await; } if !did_pass { panic!( "Validation job should have finished and set the value correctly, but it did not. Must investigate!" ); } Ok(()) } // #[ignore] // #[test] // async fn validation_on_dependent_value(ctx: &mut DalContext) -> Result<()> { // let output_component = // create_component_for_default_schema_name_in_default_view(ctx, "ValidatedOutput", "Output") // .await?; // let input_component = // create_component_for_default_schema_name_in_default_view(ctx, "ValidatedInput", "Input") // .await?; // ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // connect_components_with_socket_names( // ctx, // output_component.id(), // "number", // input_component.id(), // "number", // ) // .await?; // let prop_path = &["root", "domain", "a_number"]; // let av_id = output_component // .attribute_values_for_prop(ctx, prop_path) // .await? // .pop() // .expect("there should only be one value id"); // AttributeValue::update(ctx, av_id, Some(json!(1))).await?; // ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // let source_prop_view = PropEditorTestView::for_component_id(ctx, output_component.id()) // .await? // .get_value(prop_path)?; // let destination_prop_view = PropEditorTestView::for_component_id(ctx, input_component.id()) // .await? // .get_value(prop_path)?; // // Check validations and values // let source_result = extract_value_and_validation(source_prop_view)?; // assert_eq!( // json!({ // "value": 1, // "validation": { // "status": "Success", // "message": null // } // }), // source_result // ); // let destination_result = extract_value_and_validation(destination_prop_view)?; // assert_eq!(source_result, destination_result,); // AttributeValue::update(ctx, av_id, Some(json!(3))).await?; // ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // let source_prop_view = PropEditorTestView::for_component_id(ctx, input_component.id()) // .await? // .get_value(prop_path)?; // let destination_prop_view = PropEditorTestView::for_component_id(ctx, output_component.id()) // .await? // .get_value(prop_path)?; // let source_result = extract_value_and_validation(source_prop_view)?; // assert_eq!( // json!({ // "value": 3, // "validation": { // "status": "Failure", // "message": "\"value\" must be less than or equal to 2" // } // }), // source_result // ); // let destination_result = extract_value_and_validation(destination_prop_view)?; // assert_eq!(source_result, destination_result,); // Ok(()) // } #[test] async fn multiple_changes_single_validation(ctx: &mut DalContext) -> Result<()> { let component = create_component_for_default_schema_name_in_default_view(ctx, "pirate", "Robinson Crusoe") .await?; let prop_path = &["root", "domain", "working_eyes"]; let av_id = component .attribute_values_for_prop(ctx, prop_path) .await? .pop() .expect("there should only be one value id"); AttributeValue::update(ctx, av_id, Some(json!(1))).await?; AttributeValue::update(ctx, av_id, Some(json!(3))).await?; AttributeValue::update(ctx, av_id, Some(json!(1))).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // There should be only one ValidationOutput node for attribute value { let validation_node_idxs = ctx .workspace_snapshot()? .outgoing_targets_for_edge_weight_kind( av_id, EdgeWeightKindDiscriminants::ValidationOutput, ) .await?; assert_eq!(validation_node_idxs.len(), 1); let validation_node_idx = validation_node_idxs .first() .expect("Have a validation node id"); ctx.workspace_snapshot()? .get_node_weight(*validation_node_idx) .await? .get_option_content_node_weight_of_kind(ContentAddressDiscriminants::ValidationOutput) .expect("find ValidationOutput node weight"); } Ok(()) } #[test] async fn validation_qualification(ctx: &mut DalContext) -> Result<()> { let component = create_component_for_default_schema_name_in_default_view(ctx, "pirate", "Robinson Crusoe") .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Get qualifications, should be failure (required) let validation_qualification = Component::list_qualifications(ctx, component.id()) .await? .into_iter() .find(|q| q.qualification_name == "validations") .expect("find validations qualification"); assert_eq!( json!({ "title": "Prop Validations", "output": [{ "stream": "stdout", "line": "working_eyes: \"value\" is required", "level": "log", }], "finalized": true, "description": null, "link": null, "result": { "status": "failure", "title": null, "link": null, "sub_checks": [{ "description": "Component has 1 invalid value(s).", "status": "failure", }], }, "qualificationName": "validations", }), serde_json::to_value(&validation_qualification)? ); // Update Value let prop_path = &["root", "domain", "working_eyes"]; let av_id = component .attribute_values_for_prop(ctx, prop_path) .await? .pop() .expect("there should only be one value id"); AttributeValue::update(ctx, av_id, Some(json!(1))).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Get qualifications, should be ok let validation_qualification = Component::list_qualifications(ctx, component.id()) .await? .into_iter() .find(|q| q.qualification_name == "validations") .expect("find validations qualification"); assert_eq!( json!({ "title": "Prop Validations", "output": [], "finalized": true, "description": null, "link": null, "result": { "status": "success", "title": null, "link": null, "sub_checks": [{ "description": "Component has 0 invalid value(s).", "status": "success", }], }, "qualificationName": "validations", }), serde_json::to_value(validation_qualification)? ); Ok(()) } #[test] async fn required_unset_value(ctx: &mut DalContext) -> Result<()> { ExpectSchemaVariant::create_named( ctx, "required_value", r#" function main() { return new AssetBuilder() .addProp(new PropBuilder() .setName("Value") .setKind("string") .setValidationFormat(Joi.string().required()) .build() ) .build(); } "#, ) .await; let component = create_component_for_default_schema_name_in_default_view( ctx, "required_value", "required_value", ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Get qualifications, should be failure (required) let validation_qualification = Component::list_qualifications(ctx, component.id()) .await? .into_iter() .find(|q| q.qualification_name == "validations") .expect("find validations qualification"); assert_eq!( json!({ "title": "Prop Validations", "output": [{ "stream": "stdout", "line": "Value: \"value\" is required", "level": "log", }], "description": null, "link": null, "result": { "status": "failure", "title": null, "link": null, "sub_checks": [{ "description": "Component has 1 invalid value(s).", "status": "failure", }], }, "qualificationName": "validations", "finalized": true, }), serde_json::to_value(&validation_qualification)? ); Ok(()) } #[test] async fn required_default_value(ctx: &mut DalContext) -> Result<()> { ExpectSchemaVariant::create_named( ctx, "required_default", r#" function main() { return new AssetBuilder() .addProp(new PropBuilder() .setName("Value") .setKind("string") .setDefaultValue("ok") .setValidationFormat(Joi.string().required()) .build() ) .build(); } "#, ) .await; let component = create_component_for_default_schema_name_in_default_view( ctx, "required_default", "required_default", ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Get qualifications, should be success (not required) let validation_qualification = Component::list_qualifications(ctx, component.id()) .await? .into_iter() .find(|q| q.qualification_name == "validations") .expect("find validations qualification"); assert_eq!( json!({ "title": "Prop Validations", "output": [], "finalized": true, "description": null, "link": null, "result": { "status": "success", "title": null, "link": null, "sub_checks": [{ "description": "Component has 0 invalid value(s).", "status": "success", }], }, "qualificationName": "validations", }), serde_json::to_value(&validation_qualification)? ); Ok(()) } #[test] async fn subscription_with_unresolved_value_skips_validation_on_update( ctx: &mut DalContext, ) -> Result<()> { // Create a variant with a required field that has validation ExpectSchemaVariant::create_named( ctx, "validated_subscriber", r#" function main() { return new AssetBuilder() .addProp(new PropBuilder() .setName("RequiredValue") .setKind("string") .setValidationFormat(Joi.string().required()) .build() ) .build(); } "#, ) .await; // Create subscriber and source components let subscriber_id = component::create(ctx, "validated_subscriber", "subscriber").await?; let source_id = component::create(ctx, "validated_subscriber", "source").await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Get the attribute value IDs for the required field let subscriber_av_id = Component::attribute_value_for_prop( ctx, subscriber_id, &["root", "domain", "RequiredValue"], ) .await?; // Create a subscription to the source component's RequiredValue let subscription = ValueSubscription::new( ctx, source_id, AttributePath::from_json_pointer("/domain/RequiredValue".to_string()), ) .await?; // Set the subscription (source doesn't have a value yet, so it's unresolved) AttributeValue::set_to_subscription( ctx, subscriber_av_id, subscription, None, Reason::new_user_added(ctx), ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for DVU to complete (subscription was just set) ChangeSet::wait_for_dvu(ctx, false).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for validation job to process and clear the old failed validation let seconds = 10; let mut validation_cleared = false; for _ in 0..(seconds * 10) { ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_view = PropEditorTestView::for_component_id(ctx, subscriber_id) .await? .get_value(&["root", "domain", "RequiredValue"])?; let result = extract_value_and_validation(prop_view)?; // Check if validation is absent or not a failure if result.get("validation").is_none() || result["validation"]["status"].as_str().unwrap_or("") != "Failure" { validation_cleared = true; break; } tokio::time::sleep(Duration::from_millis(100)).await; } if !validation_cleared { panic!("Validation for unresolved subscription should not show as Failure"); } // Now set a value on the source component value::set(ctx, (source_id, "/domain/RequiredValue"), "valid_value").await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for DVU to propagate the value ChangeSet::wait_for_dvu(ctx, false).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for validation job to run and show success let mut validation_passed = false; for _ in 0..(seconds * 10) { ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_view = PropEditorTestView::for_component_id(ctx, subscriber_id) .await? .get_value(&["root", "domain", "RequiredValue"])?; let result = extract_value_and_validation(prop_view)?; if result["value"] == json!("valid_value") && result.get("validation").is_some() && result["validation"]["status"] == "Success" { validation_passed = true; break; } tokio::time::sleep(Duration::from_millis(100)).await; } if !validation_passed { panic!("Validation should pass after value is resolved"); } Ok(()) } #[test] async fn subscription_with_unresolved_value_skips_validation_on_component_creation( ctx: &mut DalContext, ) -> Result<()> { // Create a variant with a required field that has validation and a source field ExpectSchemaVariant::create_named( ctx, "validated_component", r#" function main() { return new AssetBuilder() .addProp(new PropBuilder() .setName("MandatoryField") .setKind("string") .setValidationFormat(Joi.string().required()) .build() ) .addProp(new PropBuilder() .setName("SourceField") .setKind("string") .build() ) .build(); } "#, ) .await; // Create a source component (SourceField exists but has no value) let source_id = component::create(ctx, "validated_component", "source").await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Create a new subscriber component let subscriber_id = component::create(ctx, "validated_component", "subscriber").await?; // Get the attribute value ID for the mandatory field let subscriber_av_id = Component::attribute_value_for_prop( ctx, subscriber_id, &["root", "domain", "MandatoryField"], ) .await?; // Create a subscription to the source's SourceField (which exists but has no value) let subscription = ValueSubscription::new( ctx, source_id, AttributePath::from_json_pointer("/domain/SourceField".to_string()), ) .await?; // Set the subscription (source field exists but has no value yet) AttributeValue::set_to_subscription( ctx, subscriber_av_id, subscription, None, Reason::new_user_added(ctx), ) .await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for DVU to complete (subscription was just set) ChangeSet::wait_for_dvu(ctx, false).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for validation job to process let seconds = 10; let mut validation_cleared = false; for _ in 0..(seconds * 10) { ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_view = PropEditorTestView::for_component_id(ctx, subscriber_id) .await? .get_value(&["root", "domain", "MandatoryField"])?; let result = extract_value_and_validation(prop_view)?; // Check if validation is absent or not a failure if result.get("validation").is_none() || result["validation"]["status"].as_str().unwrap_or("") != "Failure" { validation_cleared = true; break; } tokio::time::sleep(Duration::from_millis(100)).await; } if !validation_cleared { panic!("Validation should not fail when subscribed property exists but has no value"); } // Now set a value on the source field value::set(ctx, (source_id, "/domain/SourceField"), "source_value").await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for DVU to propagate the value ChangeSet::wait_for_dvu(ctx, false).await?; ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; // Wait for validation job to run and show success let mut validation_passed = false; for _ in 0..(seconds * 10) { ChangeSetTestHelpers::commit_and_update_snapshot_to_visibility(ctx).await?; let prop_view = PropEditorTestView::for_component_id(ctx, subscriber_id) .await? .get_value(&["root", "domain", "MandatoryField"])?; let result = extract_value_and_validation(prop_view)?; if result["value"] == json!("source_value") && result.get("validation").is_some() && result["validation"]["status"] == "Success" { validation_passed = true; break; } tokio::time::sleep(Duration::from_millis(100)).await; } if !validation_passed { panic!("Validation should pass after subscribed value is resolved"); } Ok(()) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server