From 74c71b4204b325807205783e19077b7bc79cafaa Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 7 Sep 2026 13:31:49 -0700 Subject: [PATCH] Test nested Windows CSP candidate paths Regression coverage for #761 before the path-normalization fix. AI assistance: Codex, under maintainer direction. --- crates/context/src/detect_csp.rs | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/context/src/detect_csp.rs b/crates/context/src/detect_csp.rs index 48c510b41..db3f33829 100644 --- a/crates/context/src/detect_csp.rs +++ b/crates/context/src/detect_csp.rs @@ -197,3 +197,82 @@ pub fn run(_args: &[String], io: &mut Io) -> i32 { io.out(&format!("{}\n", json_pretty(&v))); 0 } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static NEXT_FIXTURE: AtomicUsize = AtomicUsize::new(0); + + struct Fixture(PathBuf); + + impl Fixture { + fn new() -> Self { + let root = std::env::temp_dir().join(format!( + "impeccable-csp-761-{}-{}", + std::process::id(), NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed), + )); + std::fs::create_dir(&root).unwrap(); + Self(root) + } + + fn scan(&self, path: &str, body: &str) -> Value { + let file = self.0.join(path); + std::fs::create_dir_all(file.parent().unwrap()).unwrap(); + std::fs::write(file, body).unwrap(); + detect_csp(self.0.to_str().unwrap()) + } + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + fn nested_csp_candidates_use_portable_paths() { + // Exercise the filesystem walker and native path.relative semantics, + // not just regexes with pre-normalized input. Windows CI reproduces #761. + for (path, body, shape) in [ + ("packages/app/src/security/csp.ts", "buildCSPConfig()", "append-arrays"), + ("packages/app/src/next-config.ts", "createBaseNextConfig()", "append-arrays"), + ("apps/web/svelte.config.js", "kit: { csp: { directives: {} } }", "append-arrays"), + ("apps/web/nuxt.config.ts", "'nuxt-security'; contentSecurityPolicy", "append-arrays"), + ("apps/web/next.config.mjs", "'Content-Security-Policy': 'script-src self; connect-src self'", "append-string"), + ("next.config.mjs", "'Content-Security-Policy': 'script-src self; connect-src self'", "append-string"), + ("apps/web/src/middleware.ts", "headers.set('Content-Security-Policy', policy)", "middleware"), + ("apps/web/src/layout.astro", "", "meta-tag"), + ] { + assert_eq!(Fixture::new().scan(path, body), serde_json::json!({ + "shape": shape, "signals": [path], + }), "{path}"); + } + } + + #[test] + fn unrelated_nested_files_are_not_csp_candidates() { + for (path, body) in [ + ("packages/app/src/utils/csp.ts", "buildCSPConfig()"), + ("apps/web/not-svelte.config.js", "kit: { csp: { directives: {} } }"), + ("apps/web/next.config.mjs", "'Content-Security-Policy': 'script-src self'"), + ] { + assert_eq!(Fixture::new().scan(path, body), serde_json::json!({ + "shape": null, "signals": [], + }), "{path}"); + } + } + + #[cfg(unix)] + #[test] + fn posix_backslashes_remain_literal_filename_characters() { + let result = Fixture::new().scan( + "packages/app/src/config\\notes.ts", "buildCSPConfig()", + ); + assert_eq!(result, serde_json::json!({ + "shape": "append-arrays", "signals": ["packages/app/src/config\\notes.ts"], + })); + } +}