1use crate::{
26 constraints::ConstraintEvaluationResult,
27 custom_components::{
28 ComponentExecutionResult, ComponentMetadata, CustomConstraint, CustomConstraintComponent,
29 ExecutionMetrics, ResourceQuotas, SandboxingLevel, SecurityPolicy,
30 },
31 ConstraintComponentId, Result, ShaclError,
32};
33use oxirs_core::model::Term;
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::time::{Duration, Instant};
37use wasmi::{Config, Engine, Linker, Module, Store, Val};
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct RuntimeConfig {
42 pub max_memory: usize,
44 pub max_execution_time_ms: u64,
46 pub max_stack_depth: usize,
48 pub enable_debugging: bool,
50 pub quotas: ResourceQuotas,
52}
53
54impl Default for RuntimeConfig {
55 fn default() -> Self {
56 Self {
57 max_memory: 10 * 1024 * 1024, max_execution_time_ms: 5000, max_stack_depth: 100,
60 enable_debugging: false,
61 quotas: ResourceQuotas {
62 max_cpu_time: Some(Duration::from_secs(5)),
63 max_sparql_queries: Some(10),
64 max_result_size: Some(10000),
65 max_recursion_depth: Some(100),
66 },
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
77pub struct JavaScriptValidator {
78 component_id: ConstraintComponentId,
80 metadata: ComponentMetadata,
82 code: String,
84 config: RuntimeConfig,
86 security_policy: SecurityPolicy,
88}
89
90impl JavaScriptValidator {
91 pub fn new(
93 component_id: ConstraintComponentId,
94 metadata: ComponentMetadata,
95 code: String,
96 ) -> Self {
97 Self {
98 component_id,
99 metadata,
100 code,
101 config: RuntimeConfig::default(),
102 security_policy: SecurityPolicy {
103 allow_arbitrary_sparql: false,
104 allow_external_access: false,
105 max_execution_time: Some(Duration::from_secs(5)),
106 max_memory_usage: Some(10 * 1024 * 1024),
107 allowed_sparql_operations: std::collections::HashSet::new(),
108 trusted: false,
109 sandboxing_level: SandboxingLevel::Strict,
110 resource_quotas: ResourceQuotas {
111 max_cpu_time: Some(Duration::from_secs(5)),
112 max_sparql_queries: Some(0), max_result_size: Some(1000),
114 max_recursion_depth: Some(100),
115 },
116 },
117 }
118 }
119
120 pub fn with_config(mut self, config: RuntimeConfig) -> Self {
122 self.config = config;
123 self
124 }
125
126 pub fn with_security_policy(mut self, policy: SecurityPolicy) -> Self {
128 self.security_policy = policy;
129 self
130 }
131
132 pub fn execute(
141 &self,
142 _value: &Term,
143 _parameters: &HashMap<String, Term>,
144 ) -> Result<ComponentExecutionResult> {
145 Err(ShaclError::UnsupportedOperation(format!(
146 "JavaScript constraint validator '{}' cannot be executed: no sandboxed \
147 JavaScript runtime is available in this build. Configure a WASM validator \
148 or a built-in/SPARQL constraint instead.",
149 self.component_id.as_str()
150 )))
151 }
152
153 pub fn validate_code(&self) -> Result<()> {
155 if self.code.trim().is_empty() {
158 return Err(ShaclError::Configuration(
159 "JavaScript code cannot be empty".to_string(),
160 ));
161 }
162 Ok(())
163 }
164}
165
166impl CustomConstraintComponent for JavaScriptValidator {
167 fn component_id(&self) -> &ConstraintComponentId {
168 &self.component_id
169 }
170
171 fn metadata(&self) -> &ComponentMetadata {
172 &self.metadata
173 }
174
175 fn validate_configuration(&self, _parameters: &HashMap<String, Term>) -> Result<()> {
176 self.validate_code()
177 }
178
179 fn create_constraint(&self, parameters: HashMap<String, Term>) -> Result<CustomConstraint> {
180 Ok(CustomConstraint {
181 component_id: self.component_id.clone(),
182 parameters,
183 sparql_query: None,
184 validation_function: Some("javascript".to_string()),
185 message_template: Some("JavaScript validation failed".to_string()),
186 })
187 }
188}
189
190#[derive(Debug, Clone)]
196pub struct WasmValidator {
197 component_id: ConstraintComponentId,
199 metadata: ComponentMetadata,
201 wasm_bytes: Vec<u8>,
203 config: RuntimeConfig,
205 security_policy: SecurityPolicy,
207 function_name: String,
209}
210
211impl WasmValidator {
212 pub fn new(
214 component_id: ConstraintComponentId,
215 metadata: ComponentMetadata,
216 wasm_bytes: Vec<u8>,
217 ) -> Self {
218 Self {
219 component_id,
220 metadata,
221 wasm_bytes,
222 config: RuntimeConfig::default(),
223 security_policy: SecurityPolicy {
224 allow_arbitrary_sparql: false,
225 allow_external_access: false,
226 max_execution_time: Some(Duration::from_secs(5)),
227 max_memory_usage: Some(10 * 1024 * 1024),
228 allowed_sparql_operations: std::collections::HashSet::new(),
229 trusted: false,
230 sandboxing_level: SandboxingLevel::Strict,
231 resource_quotas: ResourceQuotas {
232 max_cpu_time: Some(Duration::from_secs(5)),
233 max_sparql_queries: Some(0), max_result_size: Some(1000),
235 max_recursion_depth: Some(100),
236 },
237 },
238 function_name: "validate".to_string(),
239 }
240 }
241
242 pub fn with_config(mut self, config: RuntimeConfig) -> Self {
244 self.config = config;
245 self
246 }
247
248 pub fn with_security_policy(mut self, policy: SecurityPolicy) -> Self {
250 self.security_policy = policy;
251 self
252 }
253
254 pub fn with_function_name(mut self, name: String) -> Self {
256 self.function_name = name;
257 self
258 }
259
260 pub fn execute(
266 &self,
267 value: &Term,
268 parameters: &HashMap<String, Term>,
269 ) -> Result<ComponentExecutionResult> {
270 let start = Instant::now();
271
272 let result = self.execute_wasm_with_timeout(value, parameters)?;
274
275 let execution_time = start.elapsed();
276
277 if execution_time.as_millis() > self.config.max_execution_time_ms as u128 {
279 return Err(ShaclError::Timeout(format!(
280 "WASM execution exceeded time limit: {:?}",
281 execution_time
282 )));
283 }
284
285 Ok(result)
286 }
287
288 fn execute_wasm_with_timeout(
290 &self,
291 value: &Term,
292 _parameters: &HashMap<String, Term>,
293 ) -> Result<ComponentExecutionResult> {
294 let start = Instant::now();
295
296 let mut config = Config::default();
302 config.consume_fuel(true);
303 let engine = Engine::new(&config);
304
305 let module = Module::new(&engine, &self.wasm_bytes[..]).map_err(|e| {
307 ShaclError::Configuration(format!("Failed to compile WASM module: {}", e))
308 })?;
309
310 let linker = Linker::new(&engine);
312
313 let mut store = Store::new(&engine, ());
318 let fuel_budget = self
319 .config
320 .max_execution_time_ms
321 .saturating_mul(5_000_000)
322 .max(1_000_000);
323 store.set_fuel(fuel_budget).map_err(|e| {
324 ShaclError::Configuration(format!("Failed to set WASM fuel budget: {e}"))
325 })?;
326
327 let instance = linker
329 .instantiate_and_start(&mut store, &module)
330 .map_err(|e| {
331 ShaclError::Configuration(format!(
332 "Failed to instantiate and start WASM module: {}",
333 e
334 ))
335 })?;
336
337 let validate_func = instance
339 .get_func(&store, &self.function_name)
340 .ok_or_else(|| {
341 ShaclError::Configuration(format!(
342 "WASM module does not export function '{}'",
343 self.function_name
344 ))
345 })?;
346
347 let value_hash = self.term_to_simple_hash(value);
349
350 let mut results = [Val::I32(0)];
352 validate_func
353 .call(&mut store, &[Val::I32(value_hash)], &mut results)
354 .map_err(|e| {
355 let msg = e.to_string();
356 if msg.to_lowercase().contains("fuel") {
357 ShaclError::Timeout(format!(
358 "WASM validator exhausted its instruction budget \
359 (fuel) and was preempted: {msg}"
360 ))
361 } else {
362 ShaclError::Configuration(format!("WASM function execution failed: {msg}"))
363 }
364 })?;
365
366 let is_valid = match results[0] {
368 Val::I32(v) => v != 0,
369 _ => {
370 return Err(ShaclError::Configuration(
371 "WASM function returned unexpected type".to_string(),
372 ))
373 }
374 };
375
376 let execution_time = start.elapsed();
377
378 Ok(ComponentExecutionResult {
379 constraint_result: if is_valid {
380 ConstraintEvaluationResult::Satisfied
381 } else {
382 ConstraintEvaluationResult::Violated {
383 violating_value: Some(value.clone()),
384 message: Some("WASM validation failed".to_string()),
385 details: HashMap::new(),
386 }
387 },
388 metrics: ExecutionMetrics {
389 execution_time,
390 memory_used: 0, sparql_queries: 0,
392 success: true,
393 error: None,
394 },
395 security_violations: Vec::new(),
396 })
397 }
398
399 fn term_to_simple_hash(&self, term: &Term) -> i32 {
402 use std::collections::hash_map::DefaultHasher;
403 use std::hash::{Hash, Hasher};
404
405 let mut hasher = DefaultHasher::new();
406
407 match term {
408 Term::NamedNode(node) => {
409 "NamedNode".hash(&mut hasher);
410 node.as_str().hash(&mut hasher);
411 }
412 Term::BlankNode(node) => {
413 "BlankNode".hash(&mut hasher);
414 node.as_str().hash(&mut hasher);
415 }
416 Term::Literal(lit) => {
417 "Literal".hash(&mut hasher);
418 lit.value().hash(&mut hasher);
419 }
420 Term::Variable(var) => {
421 "Variable".hash(&mut hasher);
422 var.as_str().hash(&mut hasher);
423 }
424 Term::QuotedTriple(_) => {
425 "QuotedTriple".hash(&mut hasher);
426 }
427 }
428
429 hasher.finish() as i32
430 }
431
432 pub fn validate_module(&self) -> Result<()> {
434 if self.wasm_bytes.is_empty() {
437 return Err(ShaclError::Configuration(
438 "WASM module cannot be empty".to_string(),
439 ));
440 }
441
442 if self.wasm_bytes.len() < 4
444 || self.wasm_bytes[0] != 0x00
445 || self.wasm_bytes[1] != 0x61
446 || self.wasm_bytes[2] != 0x73
447 || self.wasm_bytes[3] != 0x6D
448 {
449 return Err(ShaclError::Configuration(
450 "Invalid WASM module: missing magic number".to_string(),
451 ));
452 }
453
454 Ok(())
455 }
456}
457
458impl CustomConstraintComponent for WasmValidator {
459 fn component_id(&self) -> &ConstraintComponentId {
460 &self.component_id
461 }
462
463 fn metadata(&self) -> &ComponentMetadata {
464 &self.metadata
465 }
466
467 fn validate_configuration(&self, _parameters: &HashMap<String, Term>) -> Result<()> {
468 self.validate_module()
469 }
470
471 fn create_constraint(&self, parameters: HashMap<String, Term>) -> Result<CustomConstraint> {
472 Ok(CustomConstraint {
473 component_id: self.component_id.clone(),
474 parameters,
475 sparql_query: None,
476 validation_function: Some("wasm".to_string()),
477 message_template: Some("WASM validation failed".to_string()),
478 })
479 }
480}
481
482pub mod interop {
484 use super::*;
485 use oxirs_core::model::{BlankNode, Literal, NamedNode};
486
487 pub fn term_to_json(term: &Term) -> serde_json::Value {
489 match term {
490 Term::NamedNode(node) => {
491 serde_json::json!({
492 "type": "NamedNode",
493 "value": node.as_str()
494 })
495 }
496 Term::BlankNode(node) => {
497 serde_json::json!({
498 "type": "BlankNode",
499 "value": node.as_str()
500 })
501 }
502 Term::Literal(lit) => {
503 serde_json::json!({
504 "type": "Literal",
505 "value": lit.value(),
506 "language": lit.language(),
507 "datatype": lit.datatype().as_str()
508 })
509 }
510 Term::Variable(var) => {
511 serde_json::json!({
512 "type": "Variable",
513 "value": var.as_str()
514 })
515 }
516 Term::QuotedTriple(_triple) => {
517 serde_json::json!({
518 "type": "QuotedTriple",
519 "value": "quoted-triple"
520 })
521 }
522 }
523 }
524
525 pub fn json_to_term(value: &serde_json::Value) -> Result<Term> {
527 let obj = value
528 .as_object()
529 .ok_or_else(|| ShaclError::Configuration("Expected JSON object".to_string()))?;
530
531 let term_type = obj
532 .get("type")
533 .and_then(|v| v.as_str())
534 .ok_or_else(|| ShaclError::Configuration("Missing 'type' field".to_string()))?;
535
536 let term_value = obj
537 .get("value")
538 .and_then(|v| v.as_str())
539 .ok_or_else(|| ShaclError::Configuration("Missing 'value' field".to_string()))?;
540
541 match term_type {
542 "NamedNode" => Ok(Term::NamedNode(NamedNode::new(term_value)?)),
543 "BlankNode" => Ok(Term::BlankNode(BlankNode::new(term_value)?)),
544 "Literal" => {
545 let language = obj.get("language").and_then(|v| v.as_str());
547 let datatype = obj.get("datatype").and_then(|v| v.as_str());
548
549 let literal = match (language, datatype) {
550 (Some(lang), _) if !lang.is_empty() => {
552 Literal::new_language_tagged_literal(term_value, lang).map_err(|e| {
553 ShaclError::Configuration(format!(
554 "Invalid language tag '{}': {}",
555 lang, e
556 ))
557 })?
558 }
559 (_, Some(dt)) if !dt.is_empty() => {
561 let datatype_node = NamedNode::new(dt).map_err(|e| {
562 ShaclError::Configuration(format!(
563 "Invalid datatype IRI '{}': {}",
564 dt, e
565 ))
566 })?;
567 Literal::new_typed_literal(term_value, datatype_node)
568 }
569 _ => Literal::new(term_value),
571 };
572
573 Ok(Term::Literal(literal))
574 }
575 _ => Err(ShaclError::Configuration(format!(
576 "Unknown term type: {}",
577 term_type
578 ))),
579 }
580 }
581}
582
583pub struct ExternalValidatorBuilder {
585 component_id: Option<ConstraintComponentId>,
586 metadata: Option<ComponentMetadata>,
587 runtime_config: RuntimeConfig,
588 security_policy: SecurityPolicy,
589}
590
591impl ExternalValidatorBuilder {
592 pub fn new() -> Self {
594 Self {
595 component_id: None,
596 metadata: None,
597 runtime_config: RuntimeConfig::default(),
598 security_policy: SecurityPolicy {
599 allow_arbitrary_sparql: false,
600 allow_external_access: false,
601 max_execution_time: Some(Duration::from_secs(5)),
602 max_memory_usage: Some(10 * 1024 * 1024),
603 allowed_sparql_operations: std::collections::HashSet::new(),
604 trusted: false,
605 sandboxing_level: SandboxingLevel::Strict,
606 resource_quotas: ResourceQuotas {
607 max_cpu_time: Some(Duration::from_secs(5)),
608 max_sparql_queries: Some(0),
609 max_result_size: Some(1000),
610 max_recursion_depth: Some(100),
611 },
612 },
613 }
614 }
615
616 pub fn component_id(mut self, id: ConstraintComponentId) -> Self {
618 self.component_id = Some(id);
619 self
620 }
621
622 pub fn metadata(mut self, metadata: ComponentMetadata) -> Self {
624 self.metadata = Some(metadata);
625 self
626 }
627
628 pub fn runtime_config(mut self, config: RuntimeConfig) -> Self {
630 self.runtime_config = config;
631 self
632 }
633
634 pub fn security_policy(mut self, policy: SecurityPolicy) -> Self {
636 self.security_policy = policy;
637 self
638 }
639
640 pub fn build_js(self, code: String) -> Result<JavaScriptValidator> {
642 let component_id = self
643 .component_id
644 .ok_or_else(|| ShaclError::Configuration("Missing component ID".to_string()))?;
645 let metadata = self
646 .metadata
647 .ok_or_else(|| ShaclError::Configuration("Missing metadata".to_string()))?;
648
649 Ok(JavaScriptValidator {
650 component_id,
651 metadata,
652 code,
653 config: self.runtime_config,
654 security_policy: self.security_policy,
655 })
656 }
657
658 pub fn build_wasm(self, wasm_bytes: Vec<u8>) -> Result<WasmValidator> {
660 let component_id = self
661 .component_id
662 .ok_or_else(|| ShaclError::Configuration("Missing component ID".to_string()))?;
663 let metadata = self
664 .metadata
665 .ok_or_else(|| ShaclError::Configuration("Missing metadata".to_string()))?;
666
667 Ok(WasmValidator {
668 component_id,
669 metadata,
670 wasm_bytes,
671 config: self.runtime_config,
672 security_policy: self.security_policy,
673 function_name: "validate".to_string(),
674 })
675 }
676}
677
678impl Default for ExternalValidatorBuilder {
679 fn default() -> Self {
680 Self::new()
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn test_js_validator_creation() {
690 let metadata = ComponentMetadata {
691 name: "TestJS".to_string(),
692 description: Some("Test JavaScript validator".to_string()),
693 version: Some("1.0.0".to_string()),
694 author: None,
695 parameters: vec![],
696 applicable_to_node_shapes: true,
697 applicable_to_property_shapes: true,
698 example: None,
699 };
700
701 let validator = JavaScriptValidator::new(
702 ConstraintComponentId("test:js".to_string()),
703 metadata,
704 "function validate(value, params) { return true; }".to_string(),
705 );
706
707 assert_eq!(validator.component_id.as_str(), "test:js");
708 assert!(validator.validate_code().is_ok());
709 }
710
711 #[test]
712 fn test_js_validator_empty_code() {
713 let metadata = ComponentMetadata {
714 name: "TestJS".to_string(),
715 description: Some("Test JavaScript validator".to_string()),
716 version: Some("1.0.0".to_string()),
717 author: None,
718 parameters: vec![],
719 applicable_to_node_shapes: true,
720 applicable_to_property_shapes: true,
721 example: None,
722 };
723
724 let validator = JavaScriptValidator::new(
725 ConstraintComponentId("test:js".to_string()),
726 metadata,
727 "".to_string(),
728 );
729
730 assert!(validator.validate_code().is_err());
731 }
732
733 #[test]
734 fn regression_js_validator_execute_is_fail_loud() {
735 use oxirs_core::model::NamedNode;
736 let metadata = ComponentMetadata {
737 name: "TestJS".to_string(),
738 description: None,
739 version: Some("1.0.0".to_string()),
740 author: None,
741 parameters: vec![],
742 applicable_to_node_shapes: true,
743 applicable_to_property_shapes: true,
744 example: None,
745 };
746 let validator = JavaScriptValidator::new(
747 ConstraintComponentId("test:js".to_string()),
748 metadata,
749 "function validate(v, p) { return false; }".to_string(),
750 );
751 let value = Term::NamedNode(NamedNode::new("http://example.org/x").expect("iri"));
752 let res = validator.execute(&value, &HashMap::new());
753 assert!(
755 matches!(res, Err(ShaclError::UnsupportedOperation(_))),
756 "JS validator must fail loud, got {res:?}"
757 );
758 }
759
760 #[test]
761 fn regression_wasm_infinite_loop_is_preempted_by_fuel() {
762 use oxirs_core::model::NamedNode;
763 let wasm_bytes: Vec<u8> = vec![
767 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, 0x01, 0x7F, 0x01, 0x7F, 0x03, 0x02, 0x01, 0x00, 0x07, 0x0C, 0x01, 0x08, 0x76, 0x61, 0x6C, 0x69, 0x64, 0x61, 0x74, 0x65, 0x00,
771 0x00, 0x0A, 0x0B, 0x01, 0x09, 0x00, 0x03, 0x40, 0x0C, 0x00, 0x0B, 0x41, 0x00,
773 0x0B, ];
775 let metadata = ComponentMetadata {
776 name: "LoopWasm".to_string(),
777 description: None,
778 version: Some("1.0.0".to_string()),
779 author: None,
780 parameters: vec![],
781 applicable_to_node_shapes: true,
782 applicable_to_property_shapes: true,
783 example: None,
784 };
785 let mut validator = WasmValidator::new(
786 ConstraintComponentId("test:loop".to_string()),
787 metadata,
788 wasm_bytes,
789 );
790 validator.config.max_execution_time_ms = 1;
792 let value = Term::NamedNode(NamedNode::new("http://example.org/x").expect("iri"));
793 let res = validator.execute(&value, &HashMap::new());
794 assert!(
795 matches!(res, Err(ShaclError::Timeout(_))),
796 "infinite-loop WASM must be preempted by fuel (Timeout), got {res:?}"
797 );
798 }
799
800 #[test]
801 fn test_wasm_validator_creation() {
802 let metadata = ComponentMetadata {
803 name: "TestWASM".to_string(),
804 description: Some("Test WASM validator".to_string()),
805 version: Some("1.0.0".to_string()),
806 author: None,
807 parameters: vec![],
808 applicable_to_node_shapes: true,
809 applicable_to_property_shapes: true,
810 example: None,
811 };
812
813 let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
815
816 let validator = WasmValidator::new(
817 ConstraintComponentId("test:wasm".to_string()),
818 metadata,
819 wasm_bytes,
820 );
821
822 assert_eq!(validator.component_id.as_str(), "test:wasm");
823 assert!(validator.validate_module().is_ok());
824 }
825
826 #[test]
827 fn test_wasm_validator_invalid_magic() {
828 let metadata = ComponentMetadata {
829 name: "TestWASM".to_string(),
830 description: Some("Test WASM validator".to_string()),
831 version: Some("1.0.0".to_string()),
832 author: None,
833 parameters: vec![],
834 applicable_to_node_shapes: true,
835 applicable_to_property_shapes: true,
836 example: None,
837 };
838
839 let wasm_bytes = vec![0x00, 0x00, 0x00, 0x00];
841
842 let validator = WasmValidator::new(
843 ConstraintComponentId("test:wasm".to_string()),
844 metadata,
845 wasm_bytes,
846 );
847
848 assert!(validator.validate_module().is_err());
849 }
850
851 #[test]
852 fn test_term_to_json_conversion() {
853 use oxirs_core::model::NamedNode;
854
855 let term = Term::NamedNode(NamedNode::new("http://example.org/test").expect("valid IRI"));
856 let json = interop::term_to_json(&term);
857
858 assert_eq!(json["type"], "NamedNode");
859 assert_eq!(json["value"], "http://example.org/test");
860 }
861
862 #[test]
863 fn test_builder_js() {
864 let metadata = ComponentMetadata {
865 name: "TestJS".to_string(),
866 description: Some("Test".to_string()),
867 version: Some("1.0.0".to_string()),
868 author: None,
869 parameters: vec![],
870 applicable_to_node_shapes: true,
871 applicable_to_property_shapes: true,
872 example: None,
873 };
874
875 let validator = ExternalValidatorBuilder::new()
876 .component_id(ConstraintComponentId("test:js".to_string()))
877 .metadata(metadata)
878 .build_js("function validate() { return true; }".to_string())
879 .expect("validation should succeed");
880
881 assert_eq!(validator.component_id.as_str(), "test:js");
882 }
883
884 #[test]
885 fn test_builder_wasm() {
886 let metadata = ComponentMetadata {
887 name: "TestWASM".to_string(),
888 description: Some("Test".to_string()),
889 version: Some("1.0.0".to_string()),
890 author: None,
891 parameters: vec![],
892 applicable_to_node_shapes: true,
893 applicable_to_property_shapes: true,
894 example: None,
895 };
896
897 let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00];
898
899 let validator = ExternalValidatorBuilder::new()
900 .component_id(ConstraintComponentId("test:wasm".to_string()))
901 .metadata(metadata)
902 .build_wasm(wasm_bytes)
903 .expect("operation should succeed");
904
905 assert_eq!(validator.component_id.as_str(), "test:wasm");
906 }
907}