diff --git a/src/runtimes/dotnet/extension.rs b/src/runtimes/dotnet/extension.rs index 3588818..675eb54 100644 --- a/src/runtimes/dotnet/extension.rs +++ b/src/runtimes/dotnet/extension.rs @@ -147,3 +147,111 @@ in the repository.\n" Ok(warnings) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::compile::parse_markdown; + + fn ctx_from(front_matter: &crate::compile::types::FrontMatter) -> CompileContext<'_> { + CompileContext::for_test(front_matter) + } + + #[test] + fn test_validate_bash_disabled_warning() { + let (fm, _) = + parse_markdown("---\nname: test\ndescription: test\ntools:\n bash: []\n---\n") + .unwrap(); + let ext = DotnetExtension::new(DotnetRuntimeConfig::Enabled(true)); + let warnings = ext.validate(&ctx_from(&fm)).unwrap(); + assert!(!warnings.is_empty()); + assert!(warnings[0].contains("tools.bash is empty")); + } + + #[test] + fn test_validate_config_and_feed_url_are_mutually_exclusive() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n config: 'nuget.config'\n feed-url: 'https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + let err = ext.validate(&ctx_from(&fm)).unwrap_err(); + assert!(err.to_string().contains("mutually exclusive")); + } + + #[test] + fn test_validate_invalid_feed_url_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n feed-url: 'https://example.com/$(SECRET)/nuget'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } + + #[test] + fn test_validate_version_injection_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n version: '$(SECRET)'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } + + #[test] + fn test_validate_global_json_sentinel_skips_injection_check() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n version: 'global.json'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_ok()); + } + + #[test] + fn test_validate_global_json_conflict_bails() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("global.json"), r#"{"sdk":{"version":"8.0.100"}}"#).unwrap(); + + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n version: '9.0.x'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + let ctx = CompileContext::for_test_with_compile_dir(&fm, tmp.path()); + let err = ext.validate(&ctx).unwrap_err(); + assert!(err.to_string().contains("global.json")); + } + + #[test] + fn test_validate_global_json_sentinel_accepted_with_file_present() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::write(tmp.path().join("global.json"), r#"{"sdk":{"version":"8.0.100"}}"#).unwrap(); + + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n version: 'global.json'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + let ctx = CompileContext::for_test_with_compile_dir(&fm, tmp.path()); + assert!(ext.validate(&ctx).is_ok()); + } + + #[test] + fn test_validate_config_injection_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n dotnet:\n config: '$(SECRET)/nuget.config'\n---\n", + ) + .unwrap(); + let dotnet = fm.runtimes.as_ref().unwrap().dotnet.as_ref().unwrap(); + let ext = DotnetExtension::new(dotnet.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } +} diff --git a/src/runtimes/lean/extension.rs b/src/runtimes/lean/extension.rs index e1e0ab1..18a3378 100644 --- a/src/runtimes/lean/extension.rs +++ b/src/runtimes/lean/extension.rs @@ -85,3 +85,21 @@ the toolchain. Lean files use the `.lean` extension.\n" Ok(warnings) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::compile::parse_markdown; + + #[test] + fn test_validate_lean_bash_disabled_emits_warning() { + let (fm, _) = + parse_markdown("---\nname: test\ndescription: test\ntools:\n bash: []\n---\n") + .unwrap(); + let ext = LeanExtension::new(LeanRuntimeConfig::Enabled(true)); + let ctx = CompileContext::for_test(&fm); + let warnings = ext.validate(&ctx).unwrap(); + assert!(!warnings.is_empty()); + assert!(warnings[0].contains("tools.bash is empty")); + } +} diff --git a/src/runtimes/node/extension.rs b/src/runtimes/node/extension.rs index 7faaa9a..1cac8ab 100644 --- a/src/runtimes/node/extension.rs +++ b/src/runtimes/node/extension.rs @@ -122,3 +122,70 @@ Node.js is installed and available. Use `node` to run scripts, \ Ok(warnings) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::compile::parse_markdown; + + fn ctx_from(front_matter: &crate::compile::types::FrontMatter) -> CompileContext<'_> { + CompileContext::for_test(front_matter) + } + + #[test] + fn test_validate_bash_disabled_warning() { + let (fm, _) = + parse_markdown("---\nname: test\ndescription: test\ntools:\n bash: []\n---\n") + .unwrap(); + let ext = NodeExtension::new(NodeRuntimeConfig::Enabled(true)); + let warnings = ext.validate(&ctx_from(&fm)).unwrap(); + assert!(!warnings.is_empty()); + assert!(warnings[0].contains("tools.bash is empty")); + } + + #[test] + fn test_validate_config_and_feed_url_are_mutually_exclusive() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n node:\n config: '.npmrc'\n feed-url: 'https://pkgs.dev.azure.com/org/project/_packaging/feed/npm/registry/'\n---\n", + ) + .unwrap(); + let node = fm.runtimes.as_ref().unwrap().node.as_ref().unwrap(); + let ext = NodeExtension::new(node.clone()); + let err = ext.validate(&ctx_from(&fm)).unwrap_err(); + assert!(err.to_string().contains("mutually exclusive")); + } + + #[test] + fn test_validate_config_only_emits_warning() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n node:\n config: '.npmrc'\n---\n", + ) + .unwrap(); + let node = fm.runtimes.as_ref().unwrap().node.as_ref().unwrap(); + let ext = NodeExtension::new(node.clone()); + let warnings = ext.validate(&ctx_from(&fm)).unwrap(); + assert!(warnings.iter().any(|w| w.contains("will not be available"))); + } + + #[test] + fn test_validate_invalid_feed_url_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n node:\n feed-url: 'pkgs.dev.azure.com/no-scheme'\n---\n", + ) + .unwrap(); + let node = fm.runtimes.as_ref().unwrap().node.as_ref().unwrap(); + let ext = NodeExtension::new(node.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } + + #[test] + fn test_validate_version_injection_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n node:\n version: '$(SECRET)'\n---\n", + ) + .unwrap(); + let node = fm.runtimes.as_ref().unwrap().node.as_ref().unwrap(); + let ext = NodeExtension::new(node.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } +} diff --git a/src/runtimes/python/extension.rs b/src/runtimes/python/extension.rs index de4992b..b5f34c5 100644 --- a/src/runtimes/python/extension.rs +++ b/src/runtimes/python/extension.rs @@ -124,3 +124,70 @@ management, install it first with `pip install uv`.\n" Ok(warnings) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::compile::parse_markdown; + + fn ctx_from(front_matter: &crate::compile::types::FrontMatter) -> CompileContext<'_> { + CompileContext::for_test(front_matter) + } + + #[test] + fn test_validate_bash_disabled_warning() { + let (fm, _) = + parse_markdown("---\nname: test\ndescription: test\ntools:\n bash: []\n---\n") + .unwrap(); + let ext = PythonExtension::new(PythonRuntimeConfig::Enabled(true)); + let warnings = ext.validate(&ctx_from(&fm)).unwrap(); + assert!(!warnings.is_empty()); + assert!(warnings[0].contains("tools.bash is empty")); + } + + #[test] + fn test_validate_config_and_feed_url_are_mutually_exclusive() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n python:\n config: 'pip.conf'\n feed-url: 'https://pkgs.dev.azure.com/org/_packaging/feed/pypi/simple/'\n---\n", + ) + .unwrap(); + let python = fm.runtimes.as_ref().unwrap().python.as_ref().unwrap(); + let ext = PythonExtension::new(python.clone()); + let err = ext.validate(&ctx_from(&fm)).unwrap_err(); + assert!(err.to_string().contains("mutually exclusive")); + } + + #[test] + fn test_validate_config_only_emits_warning() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n python:\n config: 'pip.conf'\n---\n", + ) + .unwrap(); + let python = fm.runtimes.as_ref().unwrap().python.as_ref().unwrap(); + let ext = PythonExtension::new(python.clone()); + let warnings = ext.validate(&ctx_from(&fm)).unwrap(); + assert!(warnings.iter().any(|w| w.contains("will not be available"))); + } + + #[test] + fn test_validate_invalid_feed_url_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n python:\n feed-url: 'pkgs.dev.azure.com/no-scheme'\n---\n", + ) + .unwrap(); + let python = fm.runtimes.as_ref().unwrap().python.as_ref().unwrap(); + let ext = PythonExtension::new(python.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } + + #[test] + fn test_validate_version_injection_rejected() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nruntimes:\n python:\n version: '$(SECRET)'\n---\n", + ) + .unwrap(); + let python = fm.runtimes.as_ref().unwrap().python.as_ref().unwrap(); + let ext = PythonExtension::new(python.clone()); + assert!(ext.validate(&ctx_from(&fm)).is_err()); + } +}