1use std::fmt;
23use std::path::{Path, PathBuf};
24
25pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
27 ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".coffee", ];
35
36pub const FORBIDDEN_DIRECTORIES: &[&str] = &[
38 "node_modules",
39 "npm_packages",
40 ".npm",
41 ".yarn",
42 "bower_components",
43 "jspm_packages",
44];
45
46pub const FORBIDDEN_FILES: &[&str] = &[
48 "package.json",
49 "package-lock.json",
50 "yarn.lock",
51 "pnpm-lock.yaml",
52 "bun.lockb",
53 "tsconfig.json",
54 "jsconfig.json",
55 ".babelrc",
56 "babel.config.js",
57 "webpack.config.js",
58 "rollup.config.js",
59 "vite.config.js",
60 "esbuild.config.js",
61 ".eslintrc.js",
62 ".prettierrc.js",
63];
64
65pub const DANGEROUS_JS_PATTERNS: &[&str] = &[
67 "eval(",
68 "new Function(",
69 "Function(",
70 "document.write(",
71 "innerHTML =",
72 "outerHTML =",
73 "__proto__",
74 "prototype.constructor",
75 "with (",
76 "setTimeout(\"",
77 "setInterval(\"",
78];
79
80#[derive(Debug, Clone, Default)]
82pub struct ZeroJsValidationResult {
83 pub valid: bool,
85 pub unauthorized_js_files: Vec<PathBuf>,
87 pub unauthorized_css_files: Vec<PathBuf>,
89 pub unauthorized_html_files: Vec<PathBuf>,
91 pub forbidden_directories: Vec<PathBuf>,
93 pub forbidden_tooling_files: Vec<PathBuf>,
95 pub inline_scripts_detected: Vec<InlineScriptViolation>,
97 pub external_scripts_without_manifest: Vec<String>,
99 pub dangerous_patterns: Vec<DangerousPatternViolation>,
101 pub verified_js_files: Vec<PathBuf>,
103}
104
105impl ZeroJsValidationResult {
106 #[must_use]
108 pub fn is_valid(&self) -> bool {
109 self.valid
110 && self.unauthorized_js_files.is_empty()
111 && self.unauthorized_css_files.is_empty()
112 && self.unauthorized_html_files.is_empty()
113 && self.forbidden_directories.is_empty()
114 && self.forbidden_tooling_files.is_empty()
115 && self.inline_scripts_detected.is_empty()
116 && self.external_scripts_without_manifest.is_empty()
117 && self.dangerous_patterns.is_empty()
118 }
119
120 #[must_use]
122 pub fn violation_count(&self) -> usize {
123 self.unauthorized_js_files.len()
124 + self.unauthorized_css_files.len()
125 + self.unauthorized_html_files.len()
126 + self.forbidden_directories.len()
127 + self.forbidden_tooling_files.len()
128 + self.inline_scripts_detected.len()
129 + self.external_scripts_without_manifest.len()
130 + self.dangerous_patterns.len()
131 }
132}
133
134impl fmt::Display for ZeroJsValidationResult {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 if self.is_valid() {
137 writeln!(f, "Zero-JS Validation: PASSED")?;
138 writeln!(f, " Verified JS files: {}", self.verified_js_files.len())?;
139 } else {
140 writeln!(f, "Zero-JS Validation: FAILED")?;
141 writeln!(f, " Total violations: {}", self.violation_count())?;
142
143 if !self.unauthorized_js_files.is_empty() {
144 writeln!(f, "\n Unauthorized JavaScript files:")?;
145 for path in &self.unauthorized_js_files {
146 writeln!(f, " - {}", path.display())?;
147 }
148 }
149
150 if !self.forbidden_directories.is_empty() {
151 writeln!(f, "\n Forbidden directories:")?;
152 for path in &self.forbidden_directories {
153 writeln!(f, " - {}", path.display())?;
154 }
155 }
156
157 if !self.inline_scripts_detected.is_empty() {
158 writeln!(f, "\n Inline scripts detected:")?;
159 for violation in &self.inline_scripts_detected {
160 writeln!(f, " - {}", violation)?;
161 }
162 }
163
164 if !self.dangerous_patterns.is_empty() {
165 writeln!(f, "\n Dangerous patterns:")?;
166 for violation in &self.dangerous_patterns {
167 writeln!(f, " - {}", violation)?;
168 }
169 }
170 }
171 Ok(())
172 }
173}
174
175#[derive(Debug, Clone)]
177pub struct InlineScriptViolation {
178 pub file: PathBuf,
180 pub line: usize,
182 pub preview: String,
184 pub is_wasm_generated: bool,
186}
187
188impl fmt::Display for InlineScriptViolation {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(
191 f,
192 "{}:{} - {}",
193 self.file.display(),
194 self.line,
195 self.preview
196 )
197 }
198}
199
200#[derive(Debug, Clone)]
202pub struct DangerousPatternViolation {
203 pub file: PathBuf,
205 pub line: usize,
207 pub pattern: String,
209 pub context: String,
211}
212
213impl fmt::Display for DangerousPatternViolation {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 write!(
216 f,
217 "{}:{} - '{}' in: {}",
218 self.file.display(),
219 self.line,
220 self.pattern,
221 self.context
222 )
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct ZeroJsConfig {
229 pub allowed_js_patterns: Vec<String>,
231 pub allowed_css_patterns: Vec<String>,
233 pub skip_paths: Vec<PathBuf>,
235 pub require_manifest: bool,
237 pub check_dangerous_patterns: bool,
239 pub wasm_marker: String,
241 pub allow_wasm_inline_scripts: bool,
243}
244
245impl Default for ZeroJsConfig {
246 fn default() -> Self {
247 Self {
248 allowed_js_patterns: vec![],
249 allowed_css_patterns: vec![],
250 skip_paths: vec![],
251 require_manifest: true,
252 check_dangerous_patterns: true,
253 wasm_marker: "__PROBAR_WASM_GENERATED__".to_string(),
254 allow_wasm_inline_scripts: true,
255 }
256 }
257}
258
259impl ZeroJsConfig {
260 #[must_use]
262 pub fn strict() -> Self {
263 Self {
264 allowed_js_patterns: vec![],
265 allowed_css_patterns: vec![],
266 skip_paths: vec![],
267 require_manifest: true,
268 check_dangerous_patterns: true,
269 wasm_marker: "__PROBAR_WASM_GENERATED__".to_string(),
270 allow_wasm_inline_scripts: false,
271 }
272 }
273
274 #[must_use]
276 pub fn development() -> Self {
277 Self {
278 allowed_js_patterns: vec!["*.worker.js".to_string(), "*.worklet.js".to_string()],
279 allowed_css_patterns: vec!["*.css".to_string()],
280 skip_paths: vec![],
281 require_manifest: false,
282 check_dangerous_patterns: false,
283 wasm_marker: String::new(),
284 allow_wasm_inline_scripts: true,
285 }
286 }
287
288 #[must_use]
290 pub fn with_allowed_js(mut self, pattern: impl Into<String>) -> Self {
291 self.allowed_js_patterns.push(pattern.into());
292 self
293 }
294
295 #[must_use]
297 pub fn with_skip_path(mut self, path: impl Into<PathBuf>) -> Self {
298 self.skip_paths.push(path.into());
299 self
300 }
301}
302
303#[derive(Debug, Clone)]
305pub struct ZeroJsValidator {
306 config: ZeroJsConfig,
307}
308
309impl Default for ZeroJsValidator {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315impl ZeroJsValidator {
316 #[must_use]
318 pub fn new() -> Self {
319 Self {
320 config: ZeroJsConfig::default(),
321 }
322 }
323
324 #[must_use]
326 pub fn with_config(config: ZeroJsConfig) -> Self {
327 Self { config }
328 }
329
330 pub fn validate_directory(&self, path: &Path) -> std::io::Result<ZeroJsValidationResult> {
335 let mut result = ZeroJsValidationResult {
336 valid: true,
337 ..Default::default()
338 };
339
340 self.scan_directory(path, &mut result)?;
341
342 result.valid = result.is_valid();
344
345 Ok(result)
346 }
347
348 #[must_use]
350 pub fn validate_html_content(
351 &self,
352 content: &str,
353 file_path: &Path,
354 ) -> Vec<InlineScriptViolation> {
355 let mut violations = Vec::new();
356 let mut in_script = false;
357 let mut script_start_line = 0;
358 let mut script_content = String::new();
359
360 for (line_num, line) in content.lines().enumerate() {
361 let line_num = line_num + 1; if line.contains("<script") && !line.contains("src=") {
365 in_script = true;
366 script_start_line = line_num;
367 script_content.clear();
368 }
369
370 if in_script {
372 script_content.push_str(line);
373 script_content.push('\n');
374 }
375
376 if line.contains("</script>") && in_script {
378 in_script = false;
379
380 let is_wasm_generated = script_content.contains(&self.config.wasm_marker)
382 || script_content.contains("wasm")
383 || script_content.contains("WebAssembly");
384
385 if !is_wasm_generated || !self.config.allow_wasm_inline_scripts {
386 let preview = script_content
387 .chars()
388 .take(100)
389 .collect::<String>()
390 .replace('\n', " ");
391
392 violations.push(InlineScriptViolation {
393 file: file_path.to_path_buf(),
394 line: script_start_line,
395 preview,
396 is_wasm_generated,
397 });
398 }
399 }
400 }
401
402 violations
403 }
404
405 #[must_use]
407 pub fn validate_js_content(
408 &self,
409 content: &str,
410 file_path: &Path,
411 ) -> Vec<DangerousPatternViolation> {
412 if !self.config.check_dangerous_patterns {
413 return Vec::new();
414 }
415
416 let mut violations = Vec::new();
417
418 for (line_num, line) in content.lines().enumerate() {
419 let line_num = line_num + 1; for pattern in DANGEROUS_JS_PATTERNS {
422 if line.contains(pattern) {
423 let context = line.trim().chars().take(80).collect::<String>();
424 violations.push(DangerousPatternViolation {
425 file: file_path.to_path_buf(),
426 line: line_num,
427 pattern: (*pattern).to_string(),
428 context,
429 });
430 }
431 }
432 }
433
434 violations
435 }
436
437 #[must_use]
439 pub fn has_valid_manifest(&self, js_path: &Path) -> bool {
440 let manifest_path = js_path.with_extension("js.manifest.json");
441 if !manifest_path.exists() {
442 return false;
443 }
444
445 if let Ok(content) = std::fs::read_to_string(&manifest_path) {
447 content.contains("manifest_version")
449 && content.contains("output_hash")
450 && content.contains("generation")
451 } else {
452 false
453 }
454 }
455
456 fn matches_allowed_pattern(&self, path: &Path, patterns: &[String]) -> bool {
458 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
459
460 for pattern in patterns {
461 if let Some(suffix) = pattern.strip_prefix('*') {
462 if file_name.ends_with(suffix) {
463 return true;
464 }
465 } else if let Some(prefix) = pattern.strip_suffix('*') {
466 if file_name.starts_with(prefix) {
467 return true;
468 }
469 } else if file_name == pattern {
470 return true;
471 }
472 }
473
474 false
475 }
476
477 fn should_skip(&self, path: &Path) -> bool {
479 for skip in &self.config.skip_paths {
480 if path.starts_with(skip) {
481 return true;
482 }
483 }
484 false
485 }
486
487 fn scan_directory(
489 &self,
490 dir: &Path,
491 result: &mut ZeroJsValidationResult,
492 ) -> std::io::Result<()> {
493 if !dir.is_dir() {
494 return Ok(());
495 }
496
497 if let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) {
499 if FORBIDDEN_DIRECTORIES.contains(&dir_name) {
500 result.forbidden_directories.push(dir.to_path_buf());
501 return Ok(());
502 }
503 }
504
505 for entry in std::fs::read_dir(dir)? {
506 let entry = entry?;
507 let path = entry.path();
508
509 if self.should_skip(&path) {
510 continue;
511 }
512
513 if path.is_dir() {
514 self.scan_directory(&path, result)?;
515 } else if path.is_file() {
516 self.check_file(&path, result)?;
517 }
518 }
519
520 Ok(())
521 }
522
523 fn check_file(&self, path: &Path, result: &mut ZeroJsValidationResult) -> std::io::Result<()> {
525 let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
526 let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
527
528 if FORBIDDEN_FILES.contains(&file_name) {
530 result.forbidden_tooling_files.push(path.to_path_buf());
531 return Ok(());
532 }
533
534 let ext_with_dot = format!(".{extension}");
536 let is_forbidden_ext = FORBIDDEN_EXTENSIONS.contains(&ext_with_dot.as_str());
537
538 if is_forbidden_ext {
539 if self.matches_allowed_pattern(path, &self.config.allowed_js_patterns) {
541 if self.config.require_manifest && !self.has_valid_manifest(path) {
543 result.unauthorized_js_files.push(path.to_path_buf());
544 } else {
545 result.verified_js_files.push(path.to_path_buf());
546
547 if let Ok(content) = std::fs::read_to_string(path) {
549 let violations = self.validate_js_content(&content, path);
550 result.dangerous_patterns.extend(violations);
551 }
552 }
553 } else {
554 if self.has_valid_manifest(path) {
556 result.verified_js_files.push(path.to_path_buf());
557 } else {
558 result.unauthorized_js_files.push(path.to_path_buf());
559 }
560 }
561 } else if extension == "html" || extension == "htm" {
562 if let Ok(content) = std::fs::read_to_string(path) {
564 let violations = self.validate_html_content(&content, path);
565 result.inline_scripts_detected.extend(violations);
566 }
567 } else if extension == "css" {
568 if !self.matches_allowed_pattern(path, &self.config.allowed_css_patterns) {
570 result.unauthorized_css_files.push(path.to_path_buf());
571 }
572 }
573
574 Ok(())
575 }
576
577 #[cfg(feature = "browser")]
593 pub async fn validate_page_cdp(
594 &self,
595 page: &chromiumoxide::Page,
596 ) -> Result<ZeroJsValidationResult, ZeroJsError> {
597 let mut result = ZeroJsValidationResult {
598 valid: true,
599 ..Default::default()
600 };
601
602 let scripts_json: String = page
604 .evaluate(
605 r#"
606 JSON.stringify(Array.from(document.querySelectorAll('script')).map(s => ({
607 src: s.src || '',
608 content: s.textContent || '',
609 type: s.type || ''
610 })))
611 "#,
612 )
613 .await
614 .map_err(|e| ZeroJsError::CdpError(e.to_string()))?
615 .into_value()
616 .unwrap_or_else(|_| "[]".to_string());
617
618 let scripts: Vec<serde_json::Value> = serde_json::from_str(&scripts_json)
619 .map_err(|e| ZeroJsError::ParseError(e.to_string()))?;
620
621 for script in scripts {
622 let src = script["src"].as_str().unwrap_or("");
623 let content = script["content"].as_str().unwrap_or("");
624
625 if src.is_empty() {
626 let is_wasm_generated = content.contains(&self.config.wasm_marker)
628 || content.contains("wasm")
629 || content.contains("WebAssembly");
630
631 if !is_wasm_generated || !self.config.allow_wasm_inline_scripts {
632 result.inline_scripts_detected.push(InlineScriptViolation {
633 file: PathBuf::from("(page)"),
634 line: 0,
635 preview: content.chars().take(100).collect(),
636 is_wasm_generated,
637 });
638 }
639
640 let violations =
642 self.validate_js_content(content, &PathBuf::from("(inline script)"));
643 result.dangerous_patterns.extend(violations);
644 } else {
645 result
647 .external_scripts_without_manifest
648 .push(src.to_string());
649 }
650 }
651
652 result.valid = result.is_valid();
653 Ok(result)
654 }
655}
656
657#[derive(Debug, Clone)]
659pub enum ZeroJsError {
660 CdpError(String),
662 ParseError(String),
664 IoError(String),
666}
667
668impl fmt::Display for ZeroJsError {
669 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670 match self {
671 Self::CdpError(msg) => write!(f, "CDP error: {msg}"),
672 Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
673 Self::IoError(msg) => write!(f, "IO error: {msg}"),
674 }
675 }
676}
677
678impl std::error::Error for ZeroJsError {}
679
680impl From<std::io::Error> for ZeroJsError {
681 fn from(err: std::io::Error) -> Self {
682 Self::IoError(err.to_string())
683 }
684}
685
686#[cfg(test)]
687#[allow(clippy::unwrap_used, clippy::expect_used)]
688mod tests {
689 use super::*;
690 use tempfile::TempDir;
691
692 #[test]
697 fn h0_zj_01_detect_js_file() {
698 let temp = TempDir::new().unwrap();
699 let js_path = temp.path().join("user_code.js");
700 std::fs::write(&js_path, "console.log('hello')").unwrap();
701
702 let validator = ZeroJsValidator::new();
703 let result = validator.validate_directory(temp.path()).unwrap();
704
705 assert!(!result.is_valid());
706 assert!(!result.unauthorized_js_files.is_empty());
707 }
708
709 #[test]
710 fn h0_zj_02_detect_ts_file() {
711 let temp = TempDir::new().unwrap();
712 let ts_path = temp.path().join("code.ts");
713 std::fs::write(&ts_path, "const x: number = 1;").unwrap();
714
715 let validator = ZeroJsValidator::new();
716 let result = validator.validate_directory(temp.path()).unwrap();
717
718 assert!(!result.is_valid());
719 assert!(!result.unauthorized_js_files.is_empty());
720 }
721
722 #[test]
723 fn h0_zj_03_detect_mjs_file() {
724 let temp = TempDir::new().unwrap();
725 let mjs_path = temp.path().join("module.mjs");
726 std::fs::write(&mjs_path, "export const x = 1;").unwrap();
727
728 let validator = ZeroJsValidator::new();
729 let result = validator.validate_directory(temp.path()).unwrap();
730
731 assert!(!result.is_valid());
732 }
733
734 #[test]
739 fn h0_zj_04_detect_node_modules() {
740 let temp = TempDir::new().unwrap();
741 let nm_path = temp.path().join("node_modules");
742 std::fs::create_dir(&nm_path).unwrap();
743 std::fs::write(nm_path.join("package.json"), "{}").unwrap();
744
745 let validator = ZeroJsValidator::new();
746 let result = validator.validate_directory(temp.path()).unwrap();
747
748 assert!(!result.is_valid());
749 assert!(!result.forbidden_directories.is_empty());
750 }
751
752 #[test]
753 fn h0_zj_05_detect_npm_directory() {
754 let temp = TempDir::new().unwrap();
755 let npm_path = temp.path().join(".npm");
756 std::fs::create_dir(&npm_path).unwrap();
757
758 let validator = ZeroJsValidator::new();
759 let result = validator.validate_directory(temp.path()).unwrap();
760
761 assert!(!result.is_valid());
762 }
763
764 #[test]
769 fn h0_zj_06_detect_package_json() {
770 let temp = TempDir::new().unwrap();
771 let pkg_path = temp.path().join("package.json");
772 std::fs::write(&pkg_path, r#"{"name": "test"}"#).unwrap();
773
774 let validator = ZeroJsValidator::new();
775 let result = validator.validate_directory(temp.path()).unwrap();
776
777 assert!(!result.is_valid());
778 assert!(!result.forbidden_tooling_files.is_empty());
779 }
780
781 #[test]
782 fn h0_zj_07_detect_tsconfig() {
783 let temp = TempDir::new().unwrap();
784 let cfg_path = temp.path().join("tsconfig.json");
785 std::fs::write(&cfg_path, "{}").unwrap();
786
787 let validator = ZeroJsValidator::new();
788 let result = validator.validate_directory(temp.path()).unwrap();
789
790 assert!(!result.is_valid());
791 }
792
793 #[test]
794 fn h0_zj_08_detect_webpack_config() {
795 let temp = TempDir::new().unwrap();
796 let cfg_path = temp.path().join("webpack.config.js");
797 std::fs::write(&cfg_path, "module.exports = {}").unwrap();
798
799 let validator = ZeroJsValidator::new();
800 let result = validator.validate_directory(temp.path()).unwrap();
801
802 assert!(!result.is_valid());
803 assert!(
805 !result.forbidden_tooling_files.is_empty() || !result.unauthorized_js_files.is_empty()
806 );
807 }
808
809 #[test]
814 fn h0_zj_09_detect_inline_script() {
815 let html = r#"
816<!DOCTYPE html>
817<html>
818<head>
819 <script>alert('hello')</script>
820</head>
821<body></body>
822</html>
823"#;
824 let validator = ZeroJsValidator::with_config(ZeroJsConfig {
825 allow_wasm_inline_scripts: false,
826 ..Default::default()
827 });
828 let violations = validator.validate_html_content(html, Path::new("test.html"));
829
830 assert!(!violations.is_empty());
831 assert!(violations[0].preview.contains("alert"));
832 }
833
834 #[test]
835 fn h0_zj_10_allow_wasm_inline_script() {
836 let html = r#"
837<!DOCTYPE html>
838<html>
839<head>
840 <script>
841 // __PROBAR_WASM_GENERATED__
842 WebAssembly.instantiate(module);
843 </script>
844</head>
845<body></body>
846</html>
847"#;
848 let validator = ZeroJsValidator::new();
849 let violations = validator.validate_html_content(html, Path::new("test.html"));
850
851 assert!(violations.is_empty());
853 }
854
855 #[test]
860 fn h0_zj_11_detect_eval() {
861 let js = "const result = eval('1 + 1');";
862 let validator = ZeroJsValidator::new();
863 let violations = validator.validate_js_content(js, Path::new("code.js"));
864
865 assert!(!violations.is_empty());
866 assert!(violations[0].pattern.contains("eval("));
867 }
868
869 #[test]
870 fn h0_zj_12_detect_function_constructor() {
871 let js = "const fn = new Function('return 1');";
872 let validator = ZeroJsValidator::new();
873 let violations = validator.validate_js_content(js, Path::new("code.js"));
874
875 assert!(!violations.is_empty());
876 assert!(violations[0].pattern.contains("Function("));
877 }
878
879 #[test]
880 fn h0_zj_13_detect_innerhtml() {
881 let js = "element.innerHTML = userInput;";
882 let validator = ZeroJsValidator::new();
883 let violations = validator.validate_js_content(js, Path::new("code.js"));
884
885 assert!(!violations.is_empty());
886 assert!(violations[0].pattern.contains("innerHTML"));
887 }
888
889 #[test]
890 fn h0_zj_14_detect_document_write() {
891 let js = "document.write('<script>alert(1)</script>');";
892 let validator = ZeroJsValidator::new();
893 let violations = validator.validate_js_content(js, Path::new("code.js"));
894
895 assert!(!violations.is_empty());
896 }
897
898 #[test]
899 fn h0_zj_15_detect_proto_access() {
900 let js = "obj.__proto__.polluted = true;";
901 let validator = ZeroJsValidator::new();
902 let violations = validator.validate_js_content(js, Path::new("code.js"));
903
904 assert!(!violations.is_empty());
905 }
906
907 #[test]
912 fn h0_zj_16_js_with_manifest_allowed() {
913 let temp = TempDir::new().unwrap();
914 let js_path = temp.path().join("worker.js");
915 let manifest_path = temp.path().join("worker.js.manifest.json");
916
917 std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
918 std::fs::write(
919 &manifest_path,
920 r#"{"manifest_version": 1, "output_hash": "abc123", "generation": {}}"#,
921 )
922 .unwrap();
923
924 let validator = ZeroJsValidator::new();
925 let result = validator.validate_directory(temp.path()).unwrap();
926
927 assert!(result.is_valid());
928 assert!(!result.verified_js_files.is_empty());
929 }
930
931 #[test]
932 fn h0_zj_17_js_without_manifest_rejected() {
933 let temp = TempDir::new().unwrap();
934 let js_path = temp.path().join("worker.js");
935 std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
936
937 let validator = ZeroJsValidator::new();
938 let result = validator.validate_directory(temp.path()).unwrap();
939
940 assert!(!result.is_valid());
941 assert!(!result.unauthorized_js_files.is_empty());
942 }
943
944 #[test]
949 fn h0_zj_18_strict_mode() {
950 let config = ZeroJsConfig::strict();
951 assert!(config.require_manifest);
952 assert!(config.check_dangerous_patterns);
953 assert!(!config.allow_wasm_inline_scripts);
954 }
955
956 #[test]
957 fn h0_zj_19_development_mode() {
958 let config = ZeroJsConfig::development();
959 assert!(!config.require_manifest);
960 assert!(!config.check_dangerous_patterns);
961 }
962
963 #[test]
964 fn h0_zj_20_allowed_pattern() {
965 let temp = TempDir::new().unwrap();
966 let js_path = temp.path().join("audio.worker.js");
967 std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
968
969 let config = ZeroJsConfig::default()
970 .with_allowed_js("*.worker.js")
971 .with_allowed_js("*.worklet.js");
972 let validator = ZeroJsValidator::with_config(ZeroJsConfig {
973 require_manifest: false,
974 ..config
975 });
976 let result = validator.validate_directory(temp.path()).unwrap();
977
978 assert!(result.is_valid());
980 }
981
982 #[test]
983 fn h0_zj_21_skip_path() {
984 let temp = TempDir::new().unwrap();
985 let vendor_path = temp.path().join("vendor");
986 std::fs::create_dir(&vendor_path).unwrap();
987 std::fs::write(vendor_path.join("lib.js"), "var x = 1;").unwrap();
988
989 let config = ZeroJsConfig::default().with_skip_path(vendor_path);
990 let validator = ZeroJsValidator::with_config(config);
991 let result = validator.validate_directory(temp.path()).unwrap();
992
993 assert!(result.is_valid());
995 }
996
997 #[test]
1002 fn h0_zj_22_clean_wasm_directory() {
1003 let temp = TempDir::new().unwrap();
1004
1005 std::fs::write(temp.path().join("app.wasm"), b"fake wasm").unwrap();
1007 std::fs::write(
1008 temp.path().join("index.html"),
1009 "<!DOCTYPE html><html><head></head><body></body></html>",
1010 )
1011 .unwrap();
1012
1013 let validator = ZeroJsValidator::new();
1014 let result = validator.validate_directory(temp.path()).unwrap();
1015
1016 assert!(result.is_valid());
1017 assert_eq!(result.violation_count(), 0);
1018 }
1019
1020 #[test]
1025 fn h0_zj_23_result_display_passed() {
1026 let result = ZeroJsValidationResult {
1027 valid: true,
1028 verified_js_files: vec![PathBuf::from("worker.js")],
1029 ..Default::default()
1030 };
1031
1032 let display = format!("{result}");
1033 assert!(display.contains("PASSED"));
1034 assert!(display.contains("Verified JS files: 1"));
1035 }
1036
1037 #[test]
1038 fn h0_zj_24_result_display_failed() {
1039 let result = ZeroJsValidationResult {
1040 valid: false,
1041 unauthorized_js_files: vec![PathBuf::from("bad.js")],
1042 ..Default::default()
1043 };
1044
1045 let display = format!("{result}");
1046 assert!(display.contains("FAILED"));
1047 assert!(display.contains("bad.js"));
1048 }
1049
1050 #[test]
1055 fn h0_zj_25_inline_script_violation_display() {
1056 let violation = InlineScriptViolation {
1057 file: PathBuf::from("index.html"),
1058 line: 10,
1059 preview: "alert('hello')".to_string(),
1060 is_wasm_generated: false,
1061 };
1062
1063 let display = format!("{violation}");
1064 assert!(display.contains("index.html"));
1065 assert!(display.contains("10"));
1066 assert!(display.contains("alert"));
1067 }
1068
1069 #[test]
1070 fn h0_zj_26_dangerous_pattern_display() {
1071 let violation = DangerousPatternViolation {
1072 file: PathBuf::from("code.js"),
1073 line: 5,
1074 pattern: "eval(".to_string(),
1075 context: "eval(userInput)".to_string(),
1076 };
1077
1078 let display = format!("{violation}");
1079 assert!(display.contains("code.js"));
1080 assert!(display.contains("eval("));
1081 }
1082
1083 #[test]
1088 fn h0_zj_27_error_display() {
1089 let cdp_err = ZeroJsError::CdpError("connection failed".to_string());
1090 assert!(cdp_err.to_string().contains("CDP"));
1091
1092 let parse_err = ZeroJsError::ParseError("invalid json".to_string());
1093 assert!(parse_err.to_string().contains("Parse"));
1094
1095 let io_err = ZeroJsError::IoError("file not found".to_string());
1096 assert!(io_err.to_string().contains("IO"));
1097 }
1098
1099 #[test]
1100 fn h0_zj_28_error_from_io() {
1101 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
1102 let zero_js_err: ZeroJsError = io_err.into();
1103 assert!(matches!(zero_js_err, ZeroJsError::IoError(_)));
1104 }
1105
1106 #[test]
1111 fn h0_zj_29_empty_directory() {
1112 let temp = TempDir::new().unwrap();
1113 let validator = ZeroJsValidator::new();
1114 let result = validator.validate_directory(temp.path()).unwrap();
1115
1116 assert!(result.is_valid());
1117 }
1118
1119 #[test]
1120 fn h0_zj_30_non_existent_directory() {
1121 let validator = ZeroJsValidator::new();
1122 let result = validator.validate_directory(Path::new("/nonexistent/path/12345"));
1123
1124 assert!(result.is_ok() || result.is_err());
1127 }
1128
1129 #[test]
1130 fn h0_zj_31_nested_directories() {
1131 let temp = TempDir::new().unwrap();
1132 let nested = temp.path().join("a").join("b").join("c");
1133 std::fs::create_dir_all(&nested).unwrap();
1134 std::fs::write(nested.join("deep.js"), "var x = 1;").unwrap();
1135
1136 let validator = ZeroJsValidator::new();
1137 let result = validator.validate_directory(temp.path()).unwrap();
1138
1139 assert!(!result.is_valid());
1140 assert!(!result.unauthorized_js_files.is_empty());
1141 }
1142
1143 #[test]
1144 fn h0_zj_32_manifest_invalid_json() {
1145 let temp = TempDir::new().unwrap();
1146 let js_path = temp.path().join("code.js");
1147 let manifest_path = temp.path().join("code.js.manifest.json");
1148
1149 std::fs::write(&js_path, "var x = 1;").unwrap();
1150 std::fs::write(&manifest_path, "not valid json").unwrap();
1151
1152 let validator = ZeroJsValidator::new();
1153 assert!(!validator.has_valid_manifest(&js_path));
1154 }
1155
1156 #[test]
1157 fn h0_zj_33_manifest_missing_fields() {
1158 let temp = TempDir::new().unwrap();
1159 let js_path = temp.path().join("code.js");
1160 let manifest_path = temp.path().join("code.js.manifest.json");
1161
1162 std::fs::write(&js_path, "var x = 1;").unwrap();
1163 std::fs::write(&manifest_path, r#"{"only": "partial"}"#).unwrap();
1164
1165 let validator = ZeroJsValidator::new();
1166 assert!(!validator.has_valid_manifest(&js_path));
1167 }
1168
1169 #[test]
1174 fn test_default_validator() {
1175 let validator = ZeroJsValidator::default();
1176 assert!(validator.config.require_manifest);
1177 }
1178
1179 #[test]
1180 fn test_violation_count() {
1181 let result = ZeroJsValidationResult {
1182 unauthorized_js_files: vec![PathBuf::from("a.js"), PathBuf::from("b.js")],
1183 forbidden_directories: vec![PathBuf::from("node_modules")],
1184 inline_scripts_detected: vec![InlineScriptViolation {
1185 file: PathBuf::from("x.html"),
1186 line: 1,
1187 preview: String::new(),
1188 is_wasm_generated: false,
1189 }],
1190 ..Default::default()
1191 };
1192
1193 assert_eq!(result.violation_count(), 4);
1194 }
1195
1196 #[test]
1197 fn test_with_timeout_string_pattern() {
1198 let js = r#"setTimeout("alert(1)", 1000);"#;
1199 let validator = ZeroJsValidator::new();
1200 let violations = validator.validate_js_content(js, Path::new("code.js"));
1201 assert!(!violations.is_empty());
1202 }
1203
1204 #[test]
1205 fn test_setinterval_string_pattern() {
1206 let js = r#"setInterval("tick()", 100);"#;
1207 let validator = ZeroJsValidator::new();
1208 let violations = validator.validate_js_content(js, Path::new("code.js"));
1209 assert!(!violations.is_empty());
1210 }
1211
1212 #[test]
1213 fn test_prototype_constructor_pattern() {
1214 let js = "obj.prototype.constructor = Evil;";
1215 let validator = ZeroJsValidator::new();
1216 let violations = validator.validate_js_content(js, Path::new("code.js"));
1217 assert!(!violations.is_empty());
1218 }
1219
1220 #[test]
1221 fn test_outerhtml_pattern() {
1222 let js = "el.outerHTML = userInput;";
1223 let validator = ZeroJsValidator::new();
1224 let violations = validator.validate_js_content(js, Path::new("code.js"));
1225 assert!(!violations.is_empty());
1226 }
1227
1228 #[test]
1229 fn test_with_statement_pattern() {
1230 let js = "with (obj) { x = 1; }";
1231 let validator = ZeroJsValidator::new();
1232 let violations = validator.validate_js_content(js, Path::new("code.js"));
1233 assert!(!violations.is_empty());
1234 }
1235
1236 #[test]
1237 fn test_clean_js_no_dangerous() {
1238 let js = "const x = 1; console.log(x);";
1239 let validator = ZeroJsValidator::new();
1240 let violations = validator.validate_js_content(js, Path::new("code.js"));
1241 assert!(violations.is_empty());
1242 }
1243
1244 #[test]
1245 fn test_check_dangerous_patterns_disabled() {
1246 let js = "eval('code');";
1247 let config = ZeroJsConfig {
1248 check_dangerous_patterns: false,
1249 ..Default::default()
1250 };
1251 let validator = ZeroJsValidator::with_config(config);
1252 let violations = validator.validate_js_content(js, Path::new("code.js"));
1253 assert!(violations.is_empty());
1254 }
1255
1256 #[test]
1257 fn test_jsx_file_detected() {
1258 let temp = TempDir::new().unwrap();
1259 std::fs::write(
1260 temp.path().join("component.jsx"),
1261 "const App = () => <div/>",
1262 )
1263 .unwrap();
1264
1265 let validator = ZeroJsValidator::new();
1266 let result = validator.validate_directory(temp.path()).unwrap();
1267 assert!(!result.is_valid());
1268 }
1269
1270 #[test]
1271 fn test_tsx_file_detected() {
1272 let temp = TempDir::new().unwrap();
1273 std::fs::write(
1274 temp.path().join("component.tsx"),
1275 "const App: React.FC = () => <div/>",
1276 )
1277 .unwrap();
1278
1279 let validator = ZeroJsValidator::new();
1280 let result = validator.validate_directory(temp.path()).unwrap();
1281 assert!(!result.is_valid());
1282 }
1283
1284 #[test]
1285 fn test_coffee_file_detected() {
1286 let temp = TempDir::new().unwrap();
1287 std::fs::write(temp.path().join("app.coffee"), "x = 1").unwrap();
1288
1289 let validator = ZeroJsValidator::new();
1290 let result = validator.validate_directory(temp.path()).unwrap();
1291 assert!(!result.is_valid());
1292 }
1293
1294 #[test]
1295 fn test_cjs_file_detected() {
1296 let temp = TempDir::new().unwrap();
1297 std::fs::write(temp.path().join("module.cjs"), "module.exports = {}").unwrap();
1298
1299 let validator = ZeroJsValidator::new();
1300 let result = validator.validate_directory(temp.path()).unwrap();
1301 assert!(!result.is_valid());
1302 }
1303
1304 #[test]
1305 fn test_yarn_directory_detected() {
1306 let temp = TempDir::new().unwrap();
1307 std::fs::create_dir(temp.path().join(".yarn")).unwrap();
1308
1309 let validator = ZeroJsValidator::new();
1310 let result = validator.validate_directory(temp.path()).unwrap();
1311 assert!(!result.is_valid());
1312 }
1313
1314 #[test]
1315 fn test_pattern_matching_prefix() {
1316 let validator = ZeroJsValidator::new();
1317 assert!(
1319 validator.matches_allowed_pattern(Path::new("bundle.abc"), &["bundle.*".to_string()])
1320 );
1321 }
1322
1323 #[test]
1324 fn test_pattern_matching_exact() {
1325 let validator = ZeroJsValidator::new();
1326 assert!(validator.matches_allowed_pattern(Path::new("exact.js"), &["exact.js".to_string()]));
1327 }
1328
1329 #[test]
1330 fn test_pattern_matching_no_match() {
1331 let validator = ZeroJsValidator::new();
1332 assert!(
1333 !validator.matches_allowed_pattern(Path::new("other.js"), &["exact.js".to_string()])
1334 );
1335 }
1336
1337 #[test]
1338 fn test_result_display_with_all_violations() {
1339 let result = ZeroJsValidationResult {
1340 valid: false,
1341 unauthorized_js_files: vec![PathBuf::from("bad.js")],
1342 unauthorized_css_files: vec![],
1343 unauthorized_html_files: vec![],
1344 forbidden_directories: vec![PathBuf::from("node_modules")],
1345 forbidden_tooling_files: vec![],
1346 inline_scripts_detected: vec![InlineScriptViolation {
1347 file: PathBuf::from("index.html"),
1348 line: 5,
1349 preview: "alert('hi')".into(),
1350 is_wasm_generated: false,
1351 }],
1352 external_scripts_without_manifest: vec![],
1353 dangerous_patterns: vec![DangerousPatternViolation {
1354 file: PathBuf::from("code.js"),
1355 line: 10,
1356 pattern: "eval(".into(),
1357 context: "eval(input)".into(),
1358 }],
1359 verified_js_files: vec![],
1360 };
1361
1362 let display = format!("{result}");
1363 assert!(display.contains("FAILED"));
1364 assert!(display.contains("bad.js"));
1365 assert!(display.contains("node_modules"));
1366 assert!(display.contains("alert"));
1367 assert!(display.contains("eval"));
1368 }
1369
1370 #[test]
1371 fn test_config_with_allowed_css() {
1372 let temp = TempDir::new().unwrap();
1373 std::fs::write(temp.path().join("styles.css"), "body { color: red; }").unwrap();
1374
1375 let config = ZeroJsConfig {
1376 allowed_css_patterns: vec!["*.css".to_string()],
1377 ..Default::default()
1378 };
1379 let validator = ZeroJsValidator::with_config(config);
1380 let result = validator.validate_directory(temp.path()).unwrap();
1381
1382 assert!(result.unauthorized_css_files.is_empty());
1383 }
1384
1385 #[test]
1386 fn test_css_file_not_allowed() {
1387 let temp = TempDir::new().unwrap();
1388 std::fs::write(temp.path().join("styles.css"), "body { color: red; }").unwrap();
1389
1390 let validator = ZeroJsValidator::new();
1391 let result = validator.validate_directory(temp.path()).unwrap();
1392
1393 assert!(!result.unauthorized_css_files.is_empty());
1394 }
1395
1396 #[test]
1397 fn test_html_with_external_script() {
1398 let html = r#"
1399<!DOCTYPE html>
1400<html>
1401<head>
1402 <script src="external.js"></script>
1403</head>
1404<body></body>
1405</html>
1406"#;
1407 let validator = ZeroJsValidator::new();
1408 let violations = validator.validate_html_content(html, Path::new("test.html"));
1409
1410 assert!(violations.is_empty());
1412 }
1413
1414 #[test]
1415 fn test_html_with_wasm_marker() {
1416 let html = r#"
1417<!DOCTYPE html>
1418<html>
1419<head>
1420 <script>
1421 // __PROBAR_WASM_GENERATED__
1422 init();
1423 </script>
1424</head>
1425<body></body>
1426</html>
1427"#;
1428 let validator = ZeroJsValidator::new();
1429 let violations = validator.validate_html_content(html, Path::new("test.html"));
1430
1431 assert!(violations.is_empty());
1433 }
1434
1435 #[test]
1436 fn test_inline_script_violation_is_wasm_generated() {
1437 let violation = InlineScriptViolation {
1438 file: PathBuf::from("test.html"),
1439 line: 5,
1440 preview: "WebAssembly.instantiate".into(),
1441 is_wasm_generated: true,
1442 };
1443 assert!(violation.is_wasm_generated);
1444 }
1445
1446 #[test]
1447 fn test_forbidden_directories_all() {
1448 for dir_name in FORBIDDEN_DIRECTORIES {
1450 let temp = TempDir::new().unwrap();
1451 let forbidden = temp.path().join(dir_name);
1452 std::fs::create_dir(&forbidden).unwrap();
1453
1454 let validator = ZeroJsValidator::new();
1455 let result = validator.validate_directory(temp.path()).unwrap();
1456
1457 assert!(
1458 !result.is_valid(),
1459 "Directory '{}' should be forbidden",
1460 dir_name
1461 );
1462 }
1463 }
1464
1465 #[test]
1466 fn test_forbidden_tooling_files_all() {
1467 let tooling_files = &[
1469 "package-lock.json",
1470 "yarn.lock",
1471 "pnpm-lock.yaml",
1472 "bun.lockb",
1473 "jsconfig.json",
1474 ".babelrc",
1475 "rollup.config.js",
1476 "vite.config.js",
1477 "esbuild.config.js",
1478 ".eslintrc.js",
1479 ".prettierrc.js",
1480 ];
1481
1482 for file_name in tooling_files {
1483 let temp = TempDir::new().unwrap();
1484 std::fs::write(temp.path().join(file_name), "{}").unwrap();
1485
1486 let validator = ZeroJsValidator::new();
1487 let result = validator.validate_directory(temp.path()).unwrap();
1488
1489 assert!(
1490 !result.is_valid(),
1491 "File '{}' should be forbidden",
1492 file_name
1493 );
1494 }
1495 }
1496
1497 #[test]
1498 fn test_dangerous_patterns_all() {
1499 let code_samples = &[
1501 "eval('code');",
1502 "new Function('return 1');",
1503 "Function('code')();",
1504 "document.write('html');",
1505 "el.innerHTML = x;",
1506 "el.outerHTML = y;",
1507 "obj.__proto__.bad = true;",
1508 "Foo.prototype.constructor = Bar;",
1509 "with (obj) {}",
1510 r#"setTimeout("code", 1);"#,
1511 r#"setInterval("tick", 1);"#,
1512 ];
1513
1514 let validator = ZeroJsValidator::new();
1515
1516 for code in code_samples {
1517 let violations = validator.validate_js_content(code, Path::new("test.js"));
1518 assert!(
1519 !violations.is_empty(),
1520 "Pattern should be detected in: {}",
1521 code
1522 );
1523 }
1524 }
1525
1526 #[test]
1527 fn test_zero_js_error_std_error() {
1528 let err = ZeroJsError::CdpError("test".into());
1529 let _: &dyn std::error::Error = &err;
1530 }
1531
1532 #[test]
1533 fn test_validation_result_is_valid_comprehensive() {
1534 let mut result = ZeroJsValidationResult::default();
1535 result.valid = true;
1536 assert!(result.is_valid());
1537
1538 result.unauthorized_js_files.push(PathBuf::from("a.js"));
1539 assert!(!result.is_valid());
1540
1541 result.unauthorized_js_files.clear();
1542 result.unauthorized_css_files.push(PathBuf::from("a.css"));
1543 assert!(!result.is_valid());
1544
1545 result.unauthorized_css_files.clear();
1546 result.unauthorized_html_files.push(PathBuf::from("a.html"));
1547 assert!(!result.is_valid());
1548
1549 result.unauthorized_html_files.clear();
1550 result
1551 .forbidden_directories
1552 .push(PathBuf::from("node_modules"));
1553 assert!(!result.is_valid());
1554
1555 result.forbidden_directories.clear();
1556 result
1557 .forbidden_tooling_files
1558 .push(PathBuf::from("package.json"));
1559 assert!(!result.is_valid());
1560
1561 result.forbidden_tooling_files.clear();
1562 result.inline_scripts_detected.push(InlineScriptViolation {
1563 file: PathBuf::from("x.html"),
1564 line: 1,
1565 preview: String::new(),
1566 is_wasm_generated: false,
1567 });
1568 assert!(!result.is_valid());
1569
1570 result.inline_scripts_detected.clear();
1571 result
1572 .external_scripts_without_manifest
1573 .push("ext.js".into());
1574 assert!(!result.is_valid());
1575
1576 result.external_scripts_without_manifest.clear();
1577 result.dangerous_patterns.push(DangerousPatternViolation {
1578 file: PathBuf::from("code.js"),
1579 line: 1,
1580 pattern: "eval(".into(),
1581 context: String::new(),
1582 });
1583 assert!(!result.is_valid());
1584 }
1585
1586 #[test]
1587 fn test_file_without_extension() {
1588 let temp = TempDir::new().unwrap();
1589 std::fs::write(temp.path().join("Makefile"), "all:\n\techo hello").unwrap();
1590
1591 let validator = ZeroJsValidator::new();
1592 let result = validator.validate_directory(temp.path()).unwrap();
1593
1594 assert!(result.unauthorized_js_files.is_empty());
1596 }
1597
1598 #[test]
1599 fn test_file_with_unknown_extension() {
1600 let temp = TempDir::new().unwrap();
1601 std::fs::write(temp.path().join("data.xyz"), "some data").unwrap();
1602
1603 let validator = ZeroJsValidator::new();
1604 let result = validator.validate_directory(temp.path()).unwrap();
1605
1606 assert!(result.is_valid());
1607 }
1608
1609 #[test]
1610 fn test_htm_extension() {
1611 let temp = TempDir::new().unwrap();
1612 let html = r#"
1613<!DOCTYPE html>
1614<html>
1615<head>
1616 <script>alert('test')</script>
1617</head>
1618<body></body>
1619</html>
1620"#;
1621 std::fs::write(temp.path().join("page.htm"), html).unwrap();
1622
1623 let validator = ZeroJsValidator::with_config(ZeroJsConfig {
1624 allow_wasm_inline_scripts: false,
1625 ..Default::default()
1626 });
1627 let result = validator.validate_directory(temp.path()).unwrap();
1628
1629 assert!(!result.inline_scripts_detected.is_empty());
1630 }
1631
1632 #[test]
1633 fn test_allowed_js_with_manifest_requirement() {
1634 let temp = TempDir::new().unwrap();
1635 let js_path = temp.path().join("app.worker.js");
1636 let manifest_path = temp.path().join("app.worker.js.manifest.json");
1637
1638 std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
1639 std::fs::write(
1640 &manifest_path,
1641 r#"{"manifest_version": 1, "output_hash": "abc", "generation": {}}"#,
1642 )
1643 .unwrap();
1644
1645 let config = ZeroJsConfig::default().with_allowed_js("*.worker.js");
1646 let validator = ZeroJsValidator::with_config(config);
1647 let result = validator.validate_directory(temp.path()).unwrap();
1648
1649 assert!(result.is_valid());
1650 assert!(!result.verified_js_files.is_empty());
1651 }
1652
1653 #[test]
1654 fn test_allowed_js_without_manifest_fails() {
1655 let temp = TempDir::new().unwrap();
1656 let js_path = temp.path().join("app.worker.js");
1657 std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
1658
1659 let config = ZeroJsConfig::default().with_allowed_js("*.worker.js");
1660 let validator = ZeroJsValidator::with_config(config);
1661 let result = validator.validate_directory(temp.path()).unwrap();
1662
1663 assert!(!result.is_valid());
1665 }
1666
1667 #[test]
1668 fn test_verified_js_with_dangerous_patterns() {
1669 let temp = TempDir::new().unwrap();
1670 let js_path = temp.path().join("worker.js");
1671 let manifest_path = temp.path().join("worker.js.manifest.json");
1672
1673 std::fs::write(&js_path, "const x = eval('1')").unwrap();
1675 std::fs::write(
1676 &manifest_path,
1677 r#"{"manifest_version": 1, "output_hash": "abc", "generation": {}}"#,
1678 )
1679 .unwrap();
1680
1681 let config = ZeroJsConfig::default().with_allowed_js("*.js");
1682 let validator = ZeroJsValidator::with_config(ZeroJsConfig {
1683 require_manifest: false,
1684 ..config
1685 });
1686 let result = validator.validate_directory(temp.path()).unwrap();
1687
1688 assert!(!result.verified_js_files.is_empty());
1690 assert!(!result.dangerous_patterns.is_empty());
1691 }
1692
1693 #[test]
1694 fn test_skip_path_nested() {
1695 let temp = TempDir::new().unwrap();
1696 let vendor = temp.path().join("vendor").join("external");
1697 std::fs::create_dir_all(&vendor).unwrap();
1698 std::fs::write(vendor.join("lib.js"), "var x = 1;").unwrap();
1699
1700 let config = ZeroJsConfig::default().with_skip_path(temp.path().join("vendor"));
1701 let validator = ZeroJsValidator::with_config(config);
1702 let result = validator.validate_directory(temp.path()).unwrap();
1703
1704 assert!(result.is_valid());
1705 }
1706
1707 #[test]
1708 fn test_multiple_inline_scripts() {
1709 let html = r#"
1710<!DOCTYPE html>
1711<html>
1712<head>
1713 <script>first();</script>
1714 <script>second();</script>
1715 <script>third();</script>
1716</head>
1717<body></body>
1718</html>
1719"#;
1720 let validator = ZeroJsValidator::with_config(ZeroJsConfig {
1721 allow_wasm_inline_scripts: false,
1722 ..Default::default()
1723 });
1724 let violations = validator.validate_html_content(html, Path::new("test.html"));
1725
1726 assert_eq!(violations.len(), 3);
1727 }
1728
1729 #[test]
1730 fn test_html_script_not_closed() {
1731 let html = r#"
1732<!DOCTYPE html>
1733<html>
1734<head>
1735 <script>unclosed
1736"#;
1737 let validator = ZeroJsValidator::with_config(ZeroJsConfig {
1738 allow_wasm_inline_scripts: false,
1739 ..Default::default()
1740 });
1741 let violations = validator.validate_html_content(html, Path::new("test.html"));
1742
1743 assert!(violations.is_empty());
1745 }
1746}