Skip to main content

jugar_probar/
zero_js.rs

1//! Zero-JavaScript Validation for WASM-First Applications (PROBAR-SPEC-012)
2//!
3//! Validates that WASM applications contain NO user-generated JavaScript, CSS, or HTML.
4//! All web assets must be programmatically generated via probar-js-gen or typed builders.
5//!
6//! ## Toyota Way Application:
7//! - **Poka-Yoke**: Type system prevents user-injected code at compile time
8//! - **Jidoka**: Validation stops the line if unauthorized code detected
9//! - **Andon**: Clear error messages identify exactly what violated zero-JS policy
10//!
11//! ## Critical Defect Vectors Addressed:
12//! 1. User-created JavaScript files
13//! 2. Inline `<script>` tags not from WASM
14//! 3. External CSS files not generated by builders
15//! 4. HTML files without WASM generation markers
16//!
17//! ## References:
18//! - whisper.apr CLAUDE.md: "ABSOLUTE ZERO JAVASCRIPT"
19//! - DO-178C Section 6.3: Configuration management
20//! - OWASP: Third-party JavaScript injection prevention
21
22use std::fmt;
23use std::path::{Path, PathBuf};
24
25/// Forbidden file patterns for zero-JS WASM applications
26pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
27    ".js",     // JavaScript files (unless manifest verified)
28    ".mjs",    // ES modules
29    ".cjs",    // CommonJS modules
30    ".ts",     // TypeScript source (should be compiled to WASM)
31    ".tsx",    // TypeScript JSX
32    ".jsx",    // React JSX
33    ".coffee", // CoffeeScript
34];
35
36/// Forbidden directory names that indicate non-WASM tooling
37pub const FORBIDDEN_DIRECTORIES: &[&str] = &[
38    "node_modules",
39    "npm_packages",
40    ".npm",
41    ".yarn",
42    "bower_components",
43    "jspm_packages",
44];
45
46/// Forbidden files that indicate JavaScript tooling
47pub 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
65/// Dangerous patterns in JavaScript that should NEVER appear
66pub 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/// Zero-JS validation result
81#[derive(Debug, Clone, Default)]
82pub struct ZeroJsValidationResult {
83    /// Whether validation passed
84    pub valid: bool,
85    /// Unauthorized JavaScript files found
86    pub unauthorized_js_files: Vec<PathBuf>,
87    /// Unauthorized CSS files found
88    pub unauthorized_css_files: Vec<PathBuf>,
89    /// Unauthorized HTML files found
90    pub unauthorized_html_files: Vec<PathBuf>,
91    /// Forbidden directories found
92    pub forbidden_directories: Vec<PathBuf>,
93    /// Forbidden tooling files found
94    pub forbidden_tooling_files: Vec<PathBuf>,
95    /// Inline scripts detected in HTML
96    pub inline_scripts_detected: Vec<InlineScriptViolation>,
97    /// External script tags without manifest
98    pub external_scripts_without_manifest: Vec<String>,
99    /// Dangerous patterns found
100    pub dangerous_patterns: Vec<DangerousPatternViolation>,
101    /// Files that passed manifest verification
102    pub verified_js_files: Vec<PathBuf>,
103}
104
105impl ZeroJsValidationResult {
106    /// Check if validation passed with no violations
107    #[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    /// Get total violation count
121    #[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/// Inline script violation details
176#[derive(Debug, Clone)]
177pub struct InlineScriptViolation {
178    /// File containing the inline script
179    pub file: PathBuf,
180    /// Line number where script tag was found
181    pub line: usize,
182    /// Preview of the script content
183    pub preview: String,
184    /// Whether it's a WASM-generated script (allowed)
185    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/// Dangerous pattern violation
201#[derive(Debug, Clone)]
202pub struct DangerousPatternViolation {
203    /// File containing the pattern
204    pub file: PathBuf,
205    /// Line number
206    pub line: usize,
207    /// The dangerous pattern found
208    pub pattern: String,
209    /// Context around the pattern
210    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/// Zero-JS validator configuration
227#[derive(Debug, Clone)]
228pub struct ZeroJsConfig {
229    /// Allowed JS files (must have manifest verification)
230    pub allowed_js_patterns: Vec<String>,
231    /// Allowed CSS files (generated by builders)
232    pub allowed_css_patterns: Vec<String>,
233    /// Paths to skip during validation
234    pub skip_paths: Vec<PathBuf>,
235    /// Whether to require manifest for all JS files
236    pub require_manifest: bool,
237    /// Whether to check for dangerous patterns
238    pub check_dangerous_patterns: bool,
239    /// WASM generation marker to look for in HTML
240    pub wasm_marker: String,
241    /// Whether inline scripts are allowed if WASM-generated
242    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    /// Create strict configuration (no JS allowed at all)
261    #[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    /// Create permissive configuration (for development)
275    #[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    /// Add an allowed JS pattern
289    #[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    /// Add a path to skip
296    #[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/// Zero-JavaScript validator for WASM-first applications
304#[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    /// Create a new validator with default configuration
317    #[must_use]
318    pub fn new() -> Self {
319        Self {
320            config: ZeroJsConfig::default(),
321        }
322    }
323
324    /// Create with custom configuration
325    #[must_use]
326    pub fn with_config(config: ZeroJsConfig) -> Self {
327        Self { config }
328    }
329
330    /// Validate a directory for zero-JS compliance
331    ///
332    /// # Errors
333    /// Returns IO error if directory cannot be read
334    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        // Update valid flag based on findings
343        result.valid = result.is_valid();
344
345        Ok(result)
346    }
347
348    /// Validate HTML content for inline scripts
349    #[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; // 1-indexed
362
363            // Check for script tag start
364            if line.contains("<script") && !line.contains("src=") {
365                in_script = true;
366                script_start_line = line_num;
367                script_content.clear();
368            }
369
370            // Accumulate script content
371            if in_script {
372                script_content.push_str(line);
373                script_content.push('\n');
374            }
375
376            // Check for script tag end
377            if line.contains("</script>") && in_script {
378                in_script = false;
379
380                // Check if it's WASM-generated
381                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    /// Validate JavaScript content for dangerous patterns
406    #[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; // 1-indexed
420
421            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    /// Check if a JS file has a valid manifest
438    #[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        // Try to read and parse manifest
446        if let Ok(content) = std::fs::read_to_string(&manifest_path) {
447            // Check for required manifest fields
448            content.contains("manifest_version")
449                && content.contains("output_hash")
450                && content.contains("generation")
451        } else {
452            false
453        }
454    }
455
456    /// Check if a file matches allowed patterns
457    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    /// Check if path should be skipped
478    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    /// Recursively scan directory
488    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        // Check if this directory is forbidden
498        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    /// Check a single file for violations
524    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        // Check for forbidden tooling files
529        if FORBIDDEN_FILES.contains(&file_name) {
530            result.forbidden_tooling_files.push(path.to_path_buf());
531            return Ok(());
532        }
533
534        // Check extension
535        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            // Check if it's allowed by pattern
540            if self.matches_allowed_pattern(path, &self.config.allowed_js_patterns) {
541                // Allowed pattern - check manifest if required
542                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                    // Still check for dangerous patterns
548                    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                // Not an allowed pattern - check manifest
555                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            // Check HTML for inline scripts
563            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            // Check CSS files
569            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    /// Validate a page via CDP for zero-JS compliance
578    ///
579    /// Checks the loaded page for:
580    /// - Inline scripts
581    /// - External scripts without manifests
582    /// - Dangerous patterns
583    ///
584    /// # Example
585    /// ```ignore
586    /// use jugar_probar::ZeroJsValidator;
587    ///
588    /// let validator = ZeroJsValidator::new();
589    /// let result = validator.validate_page_cdp(&page).await?;
590    /// assert!(result.is_valid(), "Page violates zero-JS policy: {}", result);
591    /// ```
592    #[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        // Get all script elements
603        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                // Inline script
627                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                // Check for dangerous patterns
641                let violations =
642                    self.validate_js_content(content, &PathBuf::from("(inline script)"));
643                result.dangerous_patterns.extend(violations);
644            } else {
645                // External script - check if allowed
646                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/// Error type for zero-JS validation
658#[derive(Debug, Clone)]
659pub enum ZeroJsError {
660    /// CDP operation failed
661    CdpError(String),
662    /// JSON parsing failed
663    ParseError(String),
664    /// IO error
665    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    // =========================================================================
693    // H0-ZJ-01: Forbidden JS files detected
694    // =========================================================================
695
696    #[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    // =========================================================================
735    // H0-ZJ-04: Forbidden directories detected
736    // =========================================================================
737
738    #[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    // =========================================================================
765    // H0-ZJ-06: Forbidden tooling files detected
766    // =========================================================================
767
768    #[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        // Both tooling file and JS file
804        assert!(
805            !result.forbidden_tooling_files.is_empty() || !result.unauthorized_js_files.is_empty()
806        );
807    }
808
809    // =========================================================================
810    // H0-ZJ-09: Inline scripts detected
811    // =========================================================================
812
813    #[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        // WASM-generated scripts are allowed by default
852        assert!(violations.is_empty());
853    }
854
855    // =========================================================================
856    // H0-ZJ-11: Dangerous patterns detected
857    // =========================================================================
858
859    #[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    // =========================================================================
908    // H0-ZJ-16: Manifest verification
909    // =========================================================================
910
911    #[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    // =========================================================================
945    // H0-ZJ-18: Configuration options
946    // =========================================================================
947
948    #[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        // Worker.js is allowed by pattern
979        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        // vendor directory is skipped
994        assert!(result.is_valid());
995    }
996
997    // =========================================================================
998    // H0-ZJ-22: Clean directory passes
999    // =========================================================================
1000
1001    #[test]
1002    fn h0_zj_22_clean_wasm_directory() {
1003        let temp = TempDir::new().unwrap();
1004
1005        // Create valid WASM-only structure
1006        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    // =========================================================================
1021    // H0-ZJ-23: Result display
1022    // =========================================================================
1023
1024    #[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    // =========================================================================
1051    // H0-ZJ-25: Violation displays
1052    // =========================================================================
1053
1054    #[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    // =========================================================================
1084    // H0-ZJ-27: Error types
1085    // =========================================================================
1086
1087    #[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    // =========================================================================
1107    // H0-ZJ-29: Edge cases
1108    // =========================================================================
1109
1110    #[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        // Should return Ok with valid result (empty directory behavior)
1125        // or Err for IO error
1126        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    // =========================================================================
1170    // Additional coverage tests
1171    // =========================================================================
1172
1173    #[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        // Test prefix matching (pattern ending with *)
1318        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        // External scripts are not inline scripts, so no violations here
1411        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        // WASM-generated inline scripts are allowed
1432        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        // Test all forbidden directory names
1449        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        // Test a selection of forbidden tooling files
1468        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        // Test all dangerous patterns
1500        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        // File without JS extension should be fine
1595        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        // Without manifest, even allowed patterns fail
1664        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        // JS with dangerous pattern
1674        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        // File is verified but has dangerous patterns
1689        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        // Script tag not closed - no violation recorded since no </script>
1744        assert!(violations.is_empty());
1745    }
1746}