From 80b0f75f1f49795d4b8aca717dbdb990f71c034c Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 10 Oct 2025 10:25:21 +0200 Subject: [PATCH 01/19] Add `uri()` function --- .../reference/schemas/config/functions/uri.md | 283 ++++++++++++++++++ dsc/tests/dsc_functions.tests.ps1 | 32 ++ lib/dsc-lib/locales/en-us.toml | 3 + lib/dsc-lib/src/functions/mod.rs | 4 +- lib/dsc-lib/src/functions/uri.rs | 195 ++++++++++++ 5 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 docs/reference/schemas/config/functions/uri.md create mode 100644 lib/dsc-lib/src/functions/uri.rs diff --git a/docs/reference/schemas/config/functions/uri.md b/docs/reference/schemas/config/functions/uri.md new file mode 100644 index 000000000..f5ab68d9c --- /dev/null +++ b/docs/reference/schemas/config/functions/uri.md @@ -0,0 +1,283 @@ +--- +description: Reference for the 'uri' DSC configuration document function +ms.date: 01/10/2025 +ms.topic: reference +title: uri +--- + +# uri + +## Synopsis + +Creates an absolute URI by combining the baseUri and the relativeUri string. + +## Syntax + +```Syntax +uri(, ) +``` + +## Description + +The `uri()` function combines a base URI with a relative URI to create an absolute URI. The +function handles trailing and leading slashes intelligently based on the structure of the base URI. + +The behavior depends on whether the base URI ends with a trailing slash: + +- **If baseUri ends with a trailing slash (`/`)**: The result is `baseUri` followed by + `relativeUri`. If `relativeUri` also begins with a leading slash, the trailing and leading + slashes are combined into one. + +- **If baseUri doesn't end with a trailing slash**: + - If `baseUri` has no slashes after the scheme (aside from `://` or `//` at the front), the + result is `baseUri` followed by `relativeUri`. + - If `baseUri` has slashes after the scheme but doesn't end with a slash, everything from the + last slash onward is removed from `baseUri`, and the result is the modified `baseUri` followed + by `relativeUri`. + +Use this function to build API endpoints, file paths, or resource URLs dynamically from +configuration parameters. + +## Examples + +### Example 1 - Build API endpoint with trailing slash + +The following example combines a base API URL ending with a slash with a relative path. + +```yaml +# uri.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + apiBase: + type: string + defaultValue: https://api.example.com/v1/ + resourcePath: + type: string + defaultValue: users/123 +resources: +- name: Build API endpoint + type: Microsoft.DSC.Debug/Echo + properties: + output: + endpoint: "[uri(parameters('apiBase'), parameters('resourcePath'))]" +``` + +```bash +dsc config get --file uri.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Build API endpoint + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + endpoint: https://api.example.com/v1/users/123 +messages: [] +hadErrors: false +``` + +### Example 2 - Handle duplicate slashes + +The following example shows how the function automatically handles cases where both the base URI +ends with a slash and the relative URI begins with a slash. + +```yaml +# uri.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Combine URIs with duplicate slashes + type: Microsoft.DSC.Debug/Echo + properties: + output: + withDuplicateSlashes: "[uri('https://example.com/', '/api/data')]" + result: The function combines the slashes into one +``` + +```bash +dsc config get --file uri.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Combine URIs with duplicate slashes + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + withDuplicateSlashes: https://example.com/api/data + result: The function combines the slashes into one +messages: [] +hadErrors: false +``` + +### Example 3 - Replace path segments + +The following example demonstrates how `uri()` replaces the last path segment when the base URI +doesn't end with a trailing slash. It uses the [`concat()`][01] function to build the base URL. + +```yaml +# uri.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + currentVersion: + type: string + defaultValue: v1 + newVersion: + type: string + defaultValue: v2 +resources: +- name: Update API version + type: Microsoft.DSC.Debug/Echo + properties: + output: + oldEndpoint: "[concat('https://api.example.com/', parameters('currentVersion'))]" + newEndpoint: "[uri(concat('https://api.example.com/', parameters('currentVersion')), parameters('newVersion'))]" +``` + +```bash +dsc config get --file uri.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Update API version + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + oldEndpoint: https://api.example.com/v1 + newEndpoint: https://api.example.com/v2 +messages: [] +hadErrors: false +``` + +### Example 4 - Build resource URLs + +The following example shows how to use `uri()` with [`concat()`][01] to build complete resource +URLs from configuration parameters. + +```yaml +# uri.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + storageAccount: + type: string + defaultValue: mystorageaccount + containerName: + type: string + defaultValue: documents + blobName: + type: string + defaultValue: report.pdf +resources: +- name: Build blob URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + blobUrl: >- + [uri( + concat('https://', parameters('storageAccount'), '.blob.core.windows.net/'), + concat(parameters('containerName'), '/', parameters('blobName')) + )] +``` + +```bash +dsc config get --file uri.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Build blob URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + blobUrl: https://mystorageaccount.blob.core.windows.net/documents/report.pdf +messages: [] +hadErrors: false +``` + +### Example 5 - Handle query strings and ports + +The following example demonstrates that `uri()` preserves query strings and port numbers correctly. + +```yaml +# uri.example.5.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: URI with special components + type: Microsoft.DSC.Debug/Echo + properties: + output: + withPort: "[uri('https://example.com:8080/', 'api')]" + withQuery: "[uri('https://example.com/api/', 'search?q=test&limit=10')]" +``` + +```bash +dsc config get --file uri.example.5.dsc.config.yaml +``` + +```yaml +results: +- name: URI with special components + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + withPort: https://example.com:8080/api + withQuery: https://example.com/api/search?q=test&limit=10 +messages: [] +hadErrors: false +``` + +## Parameters + +### baseUri + +The base URI string. The function's behavior depends on whether this value ends with a trailing +slash. + +```yaml +Type: string +Required: true +Position: 1 +``` + +### relativeUri + +The relative URI string to add to the base URI string. This is combined with the base URI according +to the rules described in the Description section. + +```yaml +Type: string +Required: true +Position: 2 +``` + +## Output + +The `uri()` function returns a string containing the absolute URI created by combining the base URI +and relative URI according to the slash-handling rules. + +```yaml +Type: string +``` + +## Related functions + +- [`concat()`][01] - Concatenates multiple strings together +- [`format()`][02] - Creates a formatted string from a template +- [`substring()`][03] - Extracts a portion of a string +- [`replace()`][04] - Replaces text in a string +- [`split()`][05] - Splits a string into an array +- [`parameters()`][06] - Retrieves parameter values + + +[01]: ./concat.md +[02]: ./format.md +[03]: ./substring.md +[04]: ./replace.md +[05]: ./split.md +[06]: ./parameters.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index ee5208cc0..95394a284 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -831,4 +831,36 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected } + + It 'uri function works for: ' -TestCases @( + @{ expression = "[uri('https://example.com/', 'path/file.html')]"; expected = 'https://example.com/path/file.html' } + @{ expression = "[uri('https://example.com/', '/path/file.html')]"; expected = 'https://example.com/path/file.html' } + @{ expression = "[uri('https://example.com/api/v1', 'users')]"; expected = 'https://example.com/api/users' } + @{ expression = "[uri('https://example.com/api/v1', '/users')]"; expected = 'https://example.com/api/users' } + @{ expression = "[uri('https://example.com', 'path')]"; expected = 'https://example.compath' } + @{ expression = "[uri('https://example.com', '/path')]"; expected = 'https://example.com/path' } + @{ expression = "[uri('https://api.example.com/v2/resource/', 'item/123')]"; expected = 'https://api.example.com/v2/resource/item/123' } + @{ expression = "[uri('https://example.com/api/', 'search?q=test')]"; expected = 'https://example.com/api/search?q=test' } + @{ expression = "[uri('https://example.com/', '')]"; expected = 'https://example.com/' } + @{ expression = "[uri('http://example.com/', 'page.html')]"; expected = 'http://example.com/page.html' } + @{ expression = "[uri('https://example.com:8080/', 'api')]"; expected = 'https://example.com:8080/api' } + @{ expression = "[uri(concat('https://example.com', '/'), 'path')]"; expected = 'https://example.com/path' } + @{ expression = "[uri('//example.com/', 'path')]"; expected = '//example.com/path' } + @{ expression = "[uri('https://example.com/a/b/c/', 'd/e/f')]"; expected = 'https://example.com/a/b/c/d/e/f' } + @{ expression = "[uri('https://example.com/old/path', 'new')]"; expected = 'https://example.com/old/new' } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 6b06ffcd8..ba896f7ad 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -525,6 +525,9 @@ invalidArgType = "All arguments must either be arrays or objects" description = "Returns a deterministic unique string from the given strings" invoked = "uniqueString function" +[functions.uri] +description = "Creates an absolute URI by combining the baseUri and the relativeUri string" + [functions.userFunction] expectedNoParameters = "User function '%{name}' does not accept parameters" unknownUserFunction = "Unknown user function '%{name}'" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index ce3441072..2cbf0abd1 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -68,6 +68,7 @@ pub mod to_upper; pub mod r#true; pub mod union; pub mod unique_string; +pub mod uri; pub mod user_function; pub mod utc_now; pub mod variables; @@ -187,9 +188,10 @@ impl FunctionDispatcher { Box::new(to_lower::ToLower{}), Box::new(to_upper::ToUpper{}), Box::new(r#true::True{}), - Box::new(utc_now::UtcNow{}), Box::new(union::Union{}), Box::new(unique_string::UniqueString{}), + Box::new(uri::Uri{}), + Box::new(utc_now::UtcNow{}), Box::new(variables::Variables{}), ]; for function in function_list { diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs new file mode 100644 index 000000000..f50f2f829 --- /dev/null +++ b/lib/dsc-lib/src/functions/uri.rs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; +use super::Function; + +#[derive(Debug, Default)] +pub struct Uri {} + +impl Function for Uri { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "uri".to_string(), + description: t!("functions.uri.description").to_string(), + category: vec![FunctionCategory::String], + min_args: 2, + max_args: 2, + accepted_arg_ordered_types: vec![ + vec![FunctionArgKind::String], + vec![FunctionArgKind::String], + ], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::String], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let base_uri = args[0].as_str().unwrap(); + let relative_uri = args[1].as_str().unwrap(); + + let result = combine_uri(base_uri, relative_uri); + Ok(Value::String(result)) + } +} + +fn combine_uri(base_uri: &str, relative_uri: &str) -> String { + if base_uri.is_empty() { + return relative_uri.to_string(); + } + if relative_uri.is_empty() { + return base_uri.to_string(); + } + + let base_ends_with_slash = base_uri.ends_with('/'); + let relative_starts_with_slash = relative_uri.starts_with('/'); + + // Case 1: baseUri ends with trailing slash + if base_ends_with_slash { + if relative_starts_with_slash { + // Combine trailing and leading slash into one + return format!("{}{}", base_uri, &relative_uri[1..]); + } else { + // Simply concatenate + return format!("{}{}", base_uri, relative_uri); + } + } + + // Case 2: baseUri doesn't end with trailing slash + // Check if baseUri has slashes (aside from // near the front) + let scheme_end = if base_uri.starts_with("http://") || base_uri.starts_with("https://") { + base_uri.find("://").map(|pos| pos + 3).unwrap_or(0) + } else if base_uri.starts_with("//") { + 2 + } else { + 0 + }; + + let after_scheme = &base_uri[scheme_end..]; + + if let Some(last_slash_pos) = after_scheme.rfind('/') { + let base_without_last_segment = &base_uri[..scheme_end + last_slash_pos + 1]; + if relative_starts_with_slash { + format!("{}{}", &base_without_last_segment[..base_without_last_segment.len() - 1], relative_uri) + } else { + format!("{}{}", base_without_last_segment, relative_uri) + } + } else { + format!("{}{}", base_uri, relative_uri) + } +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn test_uri_basic_trailing_slash() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/', 'path/file.html')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/path/file.html"); + } + + #[test] + fn test_uri_trailing_and_leading_slash() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/', '/path/file.html')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/path/file.html"); + } + + #[test] + fn test_uri_no_trailing_slash_with_path() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/api/v1', 'users')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/api/users"); + } + + #[test] + fn test_uri_no_trailing_slash_with_leading_slash() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/api/v1', '/users')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/api/users"); + } + + #[test] + fn test_uri_no_slashes_after_scheme() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com', 'path')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.compath"); + } + + #[test] + fn test_uri_no_slashes_after_scheme_with_leading_slash() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com', '/path')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/path"); + } + + #[test] + fn test_uri_complex_path() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://api.example.com/v2/resource/', 'item/123')]", &Context::new()).unwrap(); + assert_eq!(result, "https://api.example.com/v2/resource/item/123"); + } + + #[test] + fn test_uri_query_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/api/', 'search?q=test')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/api/search?q=test"); + } + + #[test] + fn test_uri_empty_relative() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/', '')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/"); + } + + #[test] + fn test_uri_http_scheme() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('http://example.com/', 'page.html')]", &Context::new()).unwrap(); + assert_eq!(result, "http://example.com/page.html"); + } + + #[test] + fn test_uri_with_port() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com:8080/', 'api')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com:8080/api"); + } + + #[test] + fn test_uri_nested_function() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri(concat('https://example.com', '/'), 'path')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/path"); + } + + #[test] + fn test_uri_relative_protocol() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('//example.com/', 'path')]", &Context::new()).unwrap(); + assert_eq!(result, "//example.com/path"); + } + + #[test] + fn test_uri_multiple_path_segments() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/a/b/c/', 'd/e/f')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/a/b/c/d/e/f"); + } + + #[test] + fn test_uri_replace_last_segment() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/old/path', 'new')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/old/new"); + } +} From 99c1c20cdaa5697c6685bf090571171b8e9ba51f Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 10 Oct 2025 13:21:06 +0200 Subject: [PATCH 02/19] Add `uriComponent()` function --- .../schemas/config/functions/uriComponent.md | 271 ++++++++++++++++++ dsc/tests/dsc_functions.tests.ps1 | 30 ++ lib/dsc-lib/locales/en-us.toml | 3 + lib/dsc-lib/src/functions/mod.rs | 2 + lib/dsc-lib/src/functions/uri_component.rs | 156 ++++++++++ 5 files changed, 462 insertions(+) create mode 100644 docs/reference/schemas/config/functions/uriComponent.md create mode 100644 lib/dsc-lib/src/functions/uri_component.rs diff --git a/docs/reference/schemas/config/functions/uriComponent.md b/docs/reference/schemas/config/functions/uriComponent.md new file mode 100644 index 000000000..bcf0325d5 --- /dev/null +++ b/docs/reference/schemas/config/functions/uriComponent.md @@ -0,0 +1,271 @@ +--- +description: Reference for the 'uriComponent' DSC configuration document function +ms.date: 01/10/2025 +ms.topic: reference +title: uriComponent +--- + +# uriComponent + +## Synopsis + +Encodes a string for use as a URI component using percent-encoding. + +## Syntax + +```Syntax +uriComponent() +``` + +## Description + +The `uriComponent()` function encodes a string using percent-encoding (also known as URL encoding) +to make it safe for use as a component of a URI. The function encodes all characters except the +unreserved characters defined in RFC 3986: + +- **Unreserved characters** (not encoded): `A-Z`, `a-z`, `0-9`, `-`, `_`, `.`, `~` +- **All other characters** are percent-encoded as `%XX` where `XX` is the hexadecimal value + +Use this function when you need to include user-provided data, special characters, or spaces in +URLs, query strings, or other URI components. This ensures that the resulting URI is valid and that +special characters don't break the URI structure. + +## Examples + +### Example 1 - Encode query parameter value + +The following example shows how to encode a string containing spaces for use in a URL query +parameter. + +```yaml +# uricomponent.example.1.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + searchTerm: + type: string + defaultValue: hello world +resources: +- name: Build search URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + original: "[parameters('searchTerm')]" + encoded: "[uriComponent(parameters('searchTerm'))]" + fullUrl: "[concat('https://example.com/search?q=', uriComponent(parameters('searchTerm')))]" +``` + +```bash +dsc config get --file uricomponent.example.1.dsc.config.yaml +``` + +```yaml +results: +- name: Build search URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + original: hello world + encoded: hello%20world + fullUrl: https://example.com/search?q=hello%20world +messages: [] +hadErrors: false +``` + +### Example 2 - Encode email address + +The following example demonstrates encoding an email address that contains special characters. + +```yaml +# uricomponent.example.2.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + email: + type: string + defaultValue: user+tag@example.com +resources: +- name: Encode email for URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + encoded: "[uriComponent(parameters('email'))]" + mailtoLink: "[concat('mailto:', uriComponent(parameters('email')))]" +``` + +```bash +dsc config get --file uricomponent.example.2.dsc.config.yaml +``` + +```yaml +results: +- name: Encode email for URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + encoded: user%2Btag%40example.com + mailtoLink: mailto:user%2Btag%40example.com +messages: [] +hadErrors: false +``` + +### Example 3 - Encode complete URL + +The following example shows how `uriComponent()` encodes an entire URL, including the protocol, +slashes, and special characters. + +```yaml +# uricomponent.example.3.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Encode complete URL + type: Microsoft.DSC.Debug/Echo + properties: + output: + originalUrl: https://example.com/path?query=value + encodedUrl: "[uriComponent('https://example.com/path?query=value')]" +``` + +```bash +dsc config get --file uricomponent.example.3.dsc.config.yaml +``` + +```yaml +results: +- name: Encode complete URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + originalUrl: https://example.com/path?query=value + encodedUrl: https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue +messages: [] +hadErrors: false +``` + +### Example 4 - Build API request with encoded parameters + +The following example demonstrates using `uriComponent()` with [`concat()`][01] and [`uri()`][02] +to build an API URL with safely encoded query parameters. + +```yaml +# uricomponent.example.4.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +parameters: + apiBase: + type: string + defaultValue: https://api.example.com + resourcePath: + type: string + defaultValue: /users/search + nameFilter: + type: string + defaultValue: John Doe + ageFilter: + type: string + defaultValue: '30' +resources: +- name: Build API URL with query string + type: Microsoft.DSC.Debug/Echo + properties: + output: + apiUrl: >- + [concat( + uri(parameters('apiBase'), parameters('resourcePath')), + '?name=', + uriComponent(parameters('nameFilter')), + '&age=', + parameters('ageFilter') + )] +``` + +```bash +dsc config get --file uricomponent.example.4.dsc.config.yaml +``` + +```yaml +results: +- name: Build API URL with query string + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + apiUrl: https://api.example.com/users/search?name=John%20Doe&age=30 +messages: [] +hadErrors: false +``` + +### Example 5 - Unreserved characters remain unchanged + +The following example shows that unreserved characters (letters, numbers, hyphen, underscore, +period, and tilde) are not encoded. + +```yaml +# uricomponent.example.5.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Unreserved character handling + type: Microsoft.DSC.Debug/Echo + properties: + output: + original: ABCabc123-_.~ + encoded: "[uriComponent('ABCabc123-_.~')]" + identical: "[equals(uriComponent('ABCabc123-_.~'), 'ABCabc123-_.~')]" +``` + +```bash +dsc config get --file uricomponent.example.5.dsc.config.yaml +``` + +```yaml +results: +- name: Unreserved character handling + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + original: ABCabc123-_.~ + encoded: ABCabc123-_.~ + identical: true +messages: [] +hadErrors: false +``` + +## Parameters + +### stringToEncode + +The string value to encode using percent-encoding. All characters except unreserved characters +(A-Z, a-z, 0-9, -, _, ., ~) are encoded. + +```yaml +Type: string +Required: true +Position: 1 +``` + +## Output + +The `uriComponent()` function returns a string with all characters except unreserved characters +replaced with their percent-encoded equivalents (e.g., space becomes `%20`, `@` becomes `%40`). + +```yaml +Type: string +``` + +## Related functions + +- [`uri()`][02] - Combines base and relative URIs +- [`concat()`][01] - Concatenates multiple strings together +- [`format()`][03] - Creates a formatted string from a template +- [`base64()`][04] - Encodes a string to base64 +- [`parameters()`][05] - Retrieves parameter values +- [`equals()`][06] - Compares two values for equality + + +[01]: ./concat.md +[02]: ./uri.md +[03]: ./format.md +[04]: ./base64.md +[05]: ./parameters.md +[06]: ./equals.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 95394a284..684add383 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -863,4 +863,34 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected } + + It 'uriComponent function works for: ' -TestCases @( + @{ expression = "[uriComponent('hello world')]"; expected = 'hello%20world' } + @{ expression = "[uriComponent('hello@example.com')]"; expected = 'hello%40example.com' } + @{ expression = "[uriComponent('https://example.com/path?query=value')]"; expected = 'https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue' } + @{ expression = "[uriComponent('')]"; expected = '' } + @{ expression = "[uriComponent('ABCabc123-_.~')]"; expected = 'ABCabc123-_.~' } + @{ expression = "[uriComponent(':/?#[]@!$&()*+,;=')]"; expected = '%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D' } + @{ expression = "[uriComponent('café')]"; expected = 'caf%C3%A9' } + @{ expression = "[uriComponent('name=John Doe&age=30')]"; expected = 'name%3DJohn%20Doe%26age%3D30' } + @{ expression = "[uriComponent('/path/to/my file.txt')]"; expected = '%2Fpath%2Fto%2Fmy%20file.txt' } + @{ expression = "[uriComponent(concat('hello', ' ', 'world'))]"; expected = 'hello%20world' } + @{ expression = "[uriComponent('user+tag@example.com')]"; expected = 'user%2Btag%40example.com' } + @{ expression = "[uriComponent('1234567890')]"; expected = '1234567890' } + @{ expression = "[uriComponent('100%')]"; expected = '100%25' } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index ba896f7ad..880dc8f8a 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -528,6 +528,9 @@ invoked = "uniqueString function" [functions.uri] description = "Creates an absolute URI by combining the baseUri and the relativeUri string" +[functions.uriComponent] +description = "Encodes a URI component using percent-encoding" + [functions.userFunction] expectedNoParameters = "User function '%{name}' does not accept parameters" unknownUserFunction = "Unknown user function '%{name}'" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 2cbf0abd1..9a13525f3 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -69,6 +69,7 @@ pub mod r#true; pub mod union; pub mod unique_string; pub mod uri; +pub mod uri_component; pub mod user_function; pub mod utc_now; pub mod variables; @@ -191,6 +192,7 @@ impl FunctionDispatcher { Box::new(union::Union{}), Box::new(unique_string::UniqueString{}), Box::new(uri::Uri{}), + Box::new(uri_component::UriComponent{}), Box::new(utc_now::UtcNow{}), Box::new(variables::Variables{}), ]; diff --git a/lib/dsc-lib/src/functions/uri_component.rs b/lib/dsc-lib/src/functions/uri_component.rs new file mode 100644 index 000000000..dc0b66434 --- /dev/null +++ b/lib/dsc-lib/src/functions/uri_component.rs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; +use super::Function; + +#[derive(Debug, Default)] +pub struct UriComponent {} + +impl Function for UriComponent { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "uriComponent".to_string(), + description: t!("functions.uriComponent.description").to_string(), + category: vec![FunctionCategory::String], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::String], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let string_to_encode = args[0].as_str().unwrap(); + let result = percent_encode_uri_component(string_to_encode); + Ok(Value::String(result)) + } +} + +/// Percent-encodes a string for use as a URI component. +/// +/// Encodes all characters except: +/// - Unreserved characters: A-Z, a-z, 0-9, -, _, ., ~ +/// +/// This follows RFC 3986 for URI component encoding. +fn percent_encode_uri_component(input: &str) -> String { + let mut result = String::with_capacity(input.len() * 3); + + for byte in input.bytes() { + match byte { + // Unreserved characters (RFC 3986 section 2.3) + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + result.push(byte as char); + } + // Everything else gets percent-encoded + _ => { + result.push('%'); + result.push_str(&format!("{:02X}", byte)); + } + } + } + + result +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn test_uri_component_basic_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('hello world')]", &Context::new()).unwrap(); + assert_eq!(result, "hello%20world"); + } + + #[test] + fn test_uri_component_special_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('hello@example.com')]", &Context::new()).unwrap(); + assert_eq!(result, "hello%40example.com"); + } + + #[test] + fn test_uri_component_url() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('https://example.com/path?query=value')]", &Context::new()).unwrap(); + assert_eq!(result, "https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue"); + } + + #[test] + fn test_uri_component_empty_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('')]", &Context::new()).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_uri_component_unreserved_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('ABCabc123-_.~')]", &Context::new()).unwrap(); + assert_eq!(result, "ABCabc123-_.~"); + } + + #[test] + fn test_uri_component_reserved_characters() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent(':/?#[]@!$&()*+,;=')]", &Context::new()).unwrap(); + assert_eq!(result, "%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D"); + } + + #[test] + fn test_uri_component_unicode() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('café')]", &Context::new()).unwrap(); + assert_eq!(result, "caf%C3%A9"); + } + + #[test] + fn test_uri_component_query_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('name=John Doe&age=30')]", &Context::new()).unwrap(); + assert_eq!(result, "name%3DJohn%20Doe%26age%3D30"); + } + + #[test] + fn test_uri_component_path_with_spaces() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('/path/to/my file.txt')]", &Context::new()).unwrap(); + assert_eq!(result, "%2Fpath%2Fto%2Fmy%20file.txt"); + } + + #[test] + fn test_uri_component_nested_function() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent(concat('hello', ' ', 'world'))]", &Context::new()).unwrap(); + assert_eq!(result, "hello%20world"); + } + + #[test] + fn test_uri_component_email() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('user+tag@example.com')]", &Context::new()).unwrap(); + assert_eq!(result, "user%2Btag%40example.com"); + } + + #[test] + fn test_uri_component_numbers_only() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('1234567890')]", &Context::new()).unwrap(); + assert_eq!(result, "1234567890"); + } + + #[test] + fn test_uri_component_percent_sign() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponent('100%')]", &Context::new()).unwrap(); + assert_eq!(result, "100%25"); + } +} From 496b08056ed28d0e365d3c99eaddf8fe3256b3b6 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 10 Oct 2025 13:34:57 +0200 Subject: [PATCH 03/19] Add `uriComponentToString()` function --- .../config/functions/uriComponentToString.md | 208 ++++++++++++++++++ dsc/tests/dsc_functions.tests.ps1 | 50 +++++ lib/dsc-lib/locales/en-us.toml | 7 + lib/dsc-lib/src/functions/mod.rs | 2 + .../src/functions/uri_component_to_string.rs | 171 ++++++++++++++ 5 files changed, 438 insertions(+) create mode 100644 docs/reference/schemas/config/functions/uriComponentToString.md create mode 100644 lib/dsc-lib/src/functions/uri_component_to_string.rs diff --git a/docs/reference/schemas/config/functions/uriComponentToString.md b/docs/reference/schemas/config/functions/uriComponentToString.md new file mode 100644 index 000000000..84f425d65 --- /dev/null +++ b/docs/reference/schemas/config/functions/uriComponentToString.md @@ -0,0 +1,208 @@ +--- +description: Reference for the 'uriComponentToString' DSC configuration document function +ms.date: 10/10/2025 +ms.topic: reference +title: uriComponentToString function +--- + +# uriComponentToString + +## Synopsis + +Returns a decoded string from a URI-encoded value. + +## Syntax + +```yaml +uriComponentToString() +``` + +## Description + +The `uriComponentToString()` function decodes a URI-encoded string back to its original form. +It converts percent-encoded sequences (like `%20` for space or `%40` for `@`) back to their +original characters. This function is the inverse of [`uriComponent()`][01]. + +This function is useful when you need to decode URI components that were previously encoded, +such as query parameters, path segments, or other URI parts. + +## Examples + +### Example 1 - Decode a URI-encoded query parameter + +This example decodes a URI-encoded query parameter value back to its original string. + +```yaml +# example1.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded value + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('John%20Doe')]" +``` + +```bash +dsc config get --document example1.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded value + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: John Doe +``` + +### Example 2 - Decode a URI-encoded email address + +This example decodes a URI-encoded email address with special characters. + +```yaml +# example2.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded email + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('user%2Btag%40example.com')]" +``` + +```bash +dsc config get --document example2.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded email + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: user+tag@example.com +``` + +### Example 3 - Decode a complete URI-encoded URL + +This example decodes a completely URI-encoded URL back to its readable form. + +```yaml +# example3.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded URL + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('https%3A%2F%2Fapi.example.com%2Fusers%3Fstatus%3Dactive')]" +``` + +```bash +dsc config get --document example3.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded URL + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: https://api.example.com/users?status=active +``` + +### Example 4 - Round-trip encoding and decoding + +This example demonstrates encoding a string with [`uriComponent()`][01] and then decoding it +back with `uriComponentToString()`, showing that they are inverse operations. + +```yaml +# example4.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo round-trip result + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString(uriComponent('Hello, World!'))]" +``` + +```bash +dsc config get --document example4.dsc.yaml config get +``` + +```yaml +results: +- name: Echo round-trip result + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: Hello, World! +``` + +### Example 5 - Decode Unicode characters + +This example decodes a URI-encoded string containing UTF-8 encoded Unicode characters. + +```yaml +# example5.dsc.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Echo decoded Unicode + type: Microsoft.DSC.Debug/Echo + properties: + output: "[uriComponentToString('caf%C3%A9')]" +``` + +```bash +dsc config get --document example5.dsc.yaml config get +``` + +```yaml +results: +- name: Echo decoded Unicode + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: café +``` + +## Parameters + +### uriEncodedString + +The `uriComponentToString()` function expects a single string argument representing a +URI-encoded value. The function decodes any percent-encoded sequences (like `%20`, `%40`, etc.) +back to their original characters. + +If the encoded string contains invalid percent-encoding sequences (such as incomplete sequences +or invalid hexadecimal digits), the function returns an error. + +```yaml +Type: string +Required: true +Position: 1 +``` + +## Output + +The `uriComponentToString()` function returns the decoded string with all percent-encoded +sequences converted back to their original characters. The output is always a string. + +```yaml +Type: string +``` + +## Related functions + +The following functions are related to `uriComponentToString()`: + +- [`uriComponent()`][01] - Encodes a string for safe use in URI components (inverse operation) +- [`uri()`][02] - Combines a base URI and relative URI with intelligent path handling +- [`base64ToString()`][03] - Decodes a base64-encoded string +- [`concat()`][04] - Combines multiple strings +- [`parameters()`][05] - Returns the value of a parameter + + +[01]: uriComponent.md +[02]: uri.md +[03]: base64ToString.md +[04]: concat.md +[05]: parameters.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 684add383..b9a6e9735 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -893,4 +893,54 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected } + + It 'uriComponentToString function works for: ' -TestCases @( + @{ expression = "[uriComponentToString('hello%20world')]"; expected = 'hello world' } + @{ expression = "[uriComponentToString('hello%40example.com')]"; expected = 'hello@example.com' } + @{ expression = "[uriComponentToString('https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue')]"; expected = 'https://example.com/path?query=value' } + @{ expression = "[uriComponentToString('')]"; expected = '' } + @{ expression = "[uriComponentToString('ABCabc123-_.~')]"; expected = 'ABCabc123-_.~' } + @{ expression = "[uriComponentToString('%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D')]"; expected = ':/?#[]@!$&()*+,;=' } + @{ expression = "[uriComponentToString('caf%C3%A9')]"; expected = 'café' } + @{ expression = "[uriComponentToString('name%3DJohn%20Doe%26age%3D30')]"; expected = 'name=John Doe&age=30' } + @{ expression = "[uriComponentToString('%2Fpath%2Fto%2Fmy%20file.txt')]"; expected = '/path/to/my file.txt' } + @{ expression = "[uriComponentToString(uriComponent('hello world'))]"; expected = 'hello world' } + @{ expression = "[uriComponentToString(uriComponent('user+tag@example.com'))]"; expected = 'user+tag@example.com' } + @{ expression = "[uriComponentToString('100%25')]"; expected = '100%' } + @{ expression = "[uriComponentToString(concat('hello', '%20', 'world'))]"; expected = 'hello world' } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } + + It 'uriComponentToString function error handling: ' -TestCases @( + @{ expression = "[uriComponentToString('incomplete%2')]" ; expectedError = 'Invalid percent-encoding: incomplete sequence at position' } + @{ expression = "[uriComponentToString('invalid%ZZ')]" ; expectedError = 'Invalid percent-encoding:.*is not valid hex at position' } + ) { + param($expression, $expectedError) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: `"$expression`" +"@ + $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match $expectedError + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 880dc8f8a..ab3a546ce 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -531,6 +531,13 @@ description = "Creates an absolute URI by combining the baseUri and the relative [functions.uriComponent] description = "Encodes a URI component using percent-encoding" +[functions.uriComponentToString] +description = "Returns a string of a URI encoded value" +incompleteSequence = "Invalid percent-encoding: incomplete sequence at position %{position}" +invalidHexDigits = "Invalid percent-encoding: invalid hex digits at position %{position}" +invalidHexValue = "Invalid percent-encoding: '%{hex}' is not valid hex at position %{position}" +invalidUtf8 = "Invalid UTF-8 in decoded string: %{error}" + [functions.userFunction] expectedNoParameters = "User function '%{name}' does not accept parameters" unknownUserFunction = "Unknown user function '%{name}'" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 9a13525f3..a5fcfc0a0 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -70,6 +70,7 @@ pub mod union; pub mod unique_string; pub mod uri; pub mod uri_component; +pub mod uri_component_to_string; pub mod user_function; pub mod utc_now; pub mod variables; @@ -193,6 +194,7 @@ impl FunctionDispatcher { Box::new(unique_string::UniqueString{}), Box::new(uri::Uri{}), Box::new(uri_component::UriComponent{}), + Box::new(uri_component_to_string::UriComponentToString{}), Box::new(utc_now::UtcNow{}), Box::new(variables::Variables{}), ]; diff --git a/lib/dsc-lib/src/functions/uri_component_to_string.rs b/lib/dsc-lib/src/functions/uri_component_to_string.rs new file mode 100644 index 000000000..5511994cb --- /dev/null +++ b/lib/dsc-lib/src/functions/uri_component_to_string.rs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::DscError; +use crate::configure::context::Context; +use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; +use rust_i18n::t; +use serde_json::Value; +use super::Function; + +#[derive(Debug, Default)] +pub struct UriComponentToString {} + +impl Function for UriComponentToString { + fn get_metadata(&self) -> FunctionMetadata { + FunctionMetadata { + name: "uriComponentToString".to_string(), + description: t!("functions.uriComponentToString.description").to_string(), + category: vec![FunctionCategory::String], + min_args: 1, + max_args: 1, + accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], + remaining_arg_accepted_types: None, + return_types: vec![FunctionArgKind::String], + } + } + + fn invoke(&self, args: &[Value], _context: &Context) -> Result { + let uri_encoded_string = args[0].as_str().unwrap(); + let result = percent_decode_uri_component(uri_encoded_string)?; + Ok(Value::String(result)) + } +} + +/// Decodes a percent-encoded URI component string. +/// +/// Decodes percent-encoded sequences (e.g., %20 to space, %40 to @). +/// This is the inverse operation of percent_encode_uri_component. +fn percent_decode_uri_component(input: &str) -> Result { + let mut result = Vec::new(); + let bytes = input.as_bytes(); + let mut i = 0; + + while i < bytes.len() { + if bytes[i] == b'%' { + // Check if we have at least 2 more characters + if i + 2 >= bytes.len() { + return Err(DscError::Parser( + t!("functions.uriComponentToString.incompleteSequence", position = i).to_string() + )); + } + + // Parse the two hex digits + let hex_str = std::str::from_utf8(&bytes[i + 1..i + 3]) + .map_err(|_| DscError::Parser( + t!("functions.uriComponentToString.invalidHexDigits", position = i).to_string() + ))?; + + let decoded_byte = u8::from_str_radix(hex_str, 16) + .map_err(|_| DscError::Parser( + t!("functions.uriComponentToString.invalidHexValue", hex = hex_str, position = i).to_string() + ))?; + + result.push(decoded_byte); + i += 3; + } else { + result.push(bytes[i]); + i += 1; + } + } + + String::from_utf8(result) + .map_err(|e| DscError::Parser(t!("functions.uriComponentToString.invalidUtf8", error = e).to_string())) +} + +#[cfg(test)] +mod tests { + use crate::configure::context::Context; + use crate::parser::Statement; + + #[test] + fn test_uri_component_to_string_basic() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('hello%20world')]", &Context::new()).unwrap(); + assert_eq!(result, "hello world"); + } + + #[test] + fn test_uri_component_to_string_email() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('hello%40example.com')]", &Context::new()).unwrap(); + assert_eq!(result, "hello@example.com"); + } + + #[test] + fn test_uri_component_to_string_url() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue')]", &Context::new()).unwrap(); + assert_eq!(result, "https://example.com/path?query=value"); + } + + #[test] + fn test_uri_component_to_string_empty() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('')]", &Context::new()).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_uri_component_to_string_no_encoding() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('ABCabc123-_.~')]", &Context::new()).unwrap(); + assert_eq!(result, "ABCabc123-_.~"); + } + + #[test] + fn test_uri_component_to_string_reserved_chars() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D')]", &Context::new()).unwrap(); + assert_eq!(result, ":/?#[]@!$&()*+,;="); + } + + #[test] + fn test_uri_component_to_string_unicode() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('caf%C3%A9')]", &Context::new()).unwrap(); + assert_eq!(result, "café"); + } + + #[test] + fn test_uri_component_to_string_query_string() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('name%3DJohn%20Doe%26age%3D30')]", &Context::new()).unwrap(); + assert_eq!(result, "name=John Doe&age=30"); + } + + #[test] + fn test_uri_component_to_string_path() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('%2Fpath%2Fto%2Fmy%20file.txt')]", &Context::new()).unwrap(); + assert_eq!(result, "/path/to/my file.txt"); + } + + #[test] + fn test_uri_component_to_string_roundtrip() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString(uriComponent('hello world'))]", &Context::new()).unwrap(); + assert_eq!(result, "hello world"); + } + + #[test] + fn test_uri_component_to_string_roundtrip_email() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString(uriComponent('user+tag@example.com'))]", &Context::new()).unwrap(); + assert_eq!(result, "user+tag@example.com"); + } + + #[test] + fn test_uri_component_to_string_percent_sign() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('100%25')]", &Context::new()).unwrap(); + assert_eq!(result, "100%"); + } + + #[test] + fn test_uri_component_to_string_mixed() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uriComponentToString('hello%20world%21')]", &Context::new()).unwrap(); + assert_eq!(result, "hello world!"); + } +} From 3033deff094af5bd3fa0e48e3f4f2620dbbd4759 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 10 Oct 2025 13:46:26 +0200 Subject: [PATCH 04/19] Fix clippy old build --- lib/dsc-lib/src/functions/uri.rs | 16 +++++++--------- lib/dsc-lib/src/functions/uri_component.rs | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index f50f2f829..d82432937 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -52,17 +52,15 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> String { if base_ends_with_slash { if relative_starts_with_slash { // Combine trailing and leading slash into one - return format!("{}{}", base_uri, &relative_uri[1..]); - } else { - // Simply concatenate - return format!("{}{}", base_uri, relative_uri); + return format!("{base_uri}{}", &relative_uri[1..]); } + return format!("{base_uri}{relative_uri}"); } // Case 2: baseUri doesn't end with trailing slash // Check if baseUri has slashes (aside from // near the front) let scheme_end = if base_uri.starts_with("http://") || base_uri.starts_with("https://") { - base_uri.find("://").map(|pos| pos + 3).unwrap_or(0) + base_uri.find("://").map_or(0, |pos| pos + 3) } else if base_uri.starts_with("//") { 2 } else { @@ -72,14 +70,14 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> String { let after_scheme = &base_uri[scheme_end..]; if let Some(last_slash_pos) = after_scheme.rfind('/') { - let base_without_last_segment = &base_uri[..scheme_end + last_slash_pos + 1]; + let base_without_last_segment = &base_uri[..=(scheme_end + last_slash_pos)]; if relative_starts_with_slash { - format!("{}{}", &base_without_last_segment[..base_without_last_segment.len() - 1], relative_uri) + format!("{}{relative_uri}", &base_without_last_segment[..base_without_last_segment.len() - 1]) } else { - format!("{}{}", base_without_last_segment, relative_uri) + format!("{base_without_last_segment}{relative_uri}") } } else { - format!("{}{}", base_uri, relative_uri) + format!("{base_uri}{relative_uri}") } } diff --git a/lib/dsc-lib/src/functions/uri_component.rs b/lib/dsc-lib/src/functions/uri_component.rs index dc0b66434..105826ab5 100644 --- a/lib/dsc-lib/src/functions/uri_component.rs +++ b/lib/dsc-lib/src/functions/uri_component.rs @@ -50,7 +50,7 @@ fn percent_encode_uri_component(input: &str) -> String { // Everything else gets percent-encoded _ => { result.push('%'); - result.push_str(&format!("{:02X}", byte)); + result.push_str(&format!("{byte:02X}")); } } } From a76dc33d02d414f2a647be497f84033afcd973a0 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 10 Oct 2025 14:04:39 +0200 Subject: [PATCH 05/19] Fix clippy --- lib/dsc-lib/src/functions/uri_component.rs | 3 ++- lib/dsc-lib/src/functions/uri_component_to_string.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/dsc-lib/src/functions/uri_component.rs b/lib/dsc-lib/src/functions/uri_component.rs index 105826ab5..696fb6079 100644 --- a/lib/dsc-lib/src/functions/uri_component.rs +++ b/lib/dsc-lib/src/functions/uri_component.rs @@ -39,6 +39,7 @@ impl Function for UriComponent { /// /// This follows RFC 3986 for URI component encoding. fn percent_encode_uri_component(input: &str) -> String { + use std::fmt::Write; let mut result = String::with_capacity(input.len() * 3); for byte in input.bytes() { @@ -50,7 +51,7 @@ fn percent_encode_uri_component(input: &str) -> String { // Everything else gets percent-encoded _ => { result.push('%'); - result.push_str(&format!("{byte:02X}")); + let _ = write!(result, "{byte:02X}"); } } } diff --git a/lib/dsc-lib/src/functions/uri_component_to_string.rs b/lib/dsc-lib/src/functions/uri_component_to_string.rs index 5511994cb..b808f05dc 100644 --- a/lib/dsc-lib/src/functions/uri_component_to_string.rs +++ b/lib/dsc-lib/src/functions/uri_component_to_string.rs @@ -35,7 +35,7 @@ impl Function for UriComponentToString { /// Decodes a percent-encoded URI component string. /// /// Decodes percent-encoded sequences (e.g., %20 to space, %40 to @). -/// This is the inverse operation of percent_encode_uri_component. +/// This is the inverse operation of `percent_encode_uri_component`. fn percent_decode_uri_component(input: &str) -> Result { let mut result = Vec::new(); let bytes = input.as_bytes(); From c88a660af4c17675024c264786ed1bc99cdef24c Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 11 Oct 2025 05:30:39 +0200 Subject: [PATCH 06/19] Fix remarks --- Cargo.lock | 7 +++ Cargo.toml | 2 + lib/dsc-lib/Cargo.toml | 1 + lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/functions/uri.rs | 36 ++++++++++---- lib/dsc-lib/src/functions/uri_component.rs | 31 +----------- .../src/functions/uri_component_to_string.rs | 48 ++----------------- 7 files changed, 44 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6484811a9..45ebd2326 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,6 +675,7 @@ dependencies = [ "tree-sitter", "tree-sitter-dscexpression", "tree-sitter-rust", + "urlencoding", "uuid", "which", ] @@ -3216,6 +3217,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 74639ac0f..dc5761a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -194,6 +194,8 @@ utfx = { version = "0.1" } # dsc-lib uuid = { version = "1.18", features = ["v4"] } # dsc-lib +urlencoding = { version = "2.1" } +# dsc-lib which = { version = "8.0" } # build-only dependencies diff --git a/lib/dsc-lib/Cargo.toml b/lib/dsc-lib/Cargo.toml index 881bcd6d7..058c4a370 100644 --- a/lib/dsc-lib/Cargo.toml +++ b/lib/dsc-lib/Cargo.toml @@ -37,6 +37,7 @@ tracing-indicatif = { workspace = true } tree-sitter = { workspace = true } tree-sitter-rust = { workspace = true} uuid = { workspace = true } +urlencoding = { workspace = true } which = { workspace = true } # workspace crate dependencies dsc-lib-osinfo = { workspace = true } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 7b9aed5fa..b890f8e1f 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -530,6 +530,7 @@ invoked = "uniqueString function" [functions.uri] description = "Creates an absolute URI by combining the baseUri and the relativeUri string" +emptyBaseUri = "The baseUri parameter cannot be empty. An absolute URI requires a valid base URI." [functions.uriComponent] description = "Encodes a URI component using percent-encoding" diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index d82432937..5da134c9c 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -32,17 +32,17 @@ impl Function for Uri { let base_uri = args[0].as_str().unwrap(); let relative_uri = args[1].as_str().unwrap(); - let result = combine_uri(base_uri, relative_uri); + let result = combine_uri(base_uri, relative_uri)?; Ok(Value::String(result)) } } -fn combine_uri(base_uri: &str, relative_uri: &str) -> String { +fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { if base_uri.is_empty() { - return relative_uri.to_string(); + return Err(DscError::Parser(t!("functions.uri.emptyBaseUri").to_string())); } if relative_uri.is_empty() { - return base_uri.to_string(); + return Ok(base_uri.to_string()); } let base_ends_with_slash = base_uri.ends_with('/'); @@ -52,9 +52,9 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> String { if base_ends_with_slash { if relative_starts_with_slash { // Combine trailing and leading slash into one - return format!("{base_uri}{}", &relative_uri[1..]); + return Ok(format!("{base_uri}{}", &relative_uri[1..])); } - return format!("{base_uri}{relative_uri}"); + return Ok(format!("{base_uri}{relative_uri}")); } // Case 2: baseUri doesn't end with trailing slash @@ -72,12 +72,18 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> String { if let Some(last_slash_pos) = after_scheme.rfind('/') { let base_without_last_segment = &base_uri[..=(scheme_end + last_slash_pos)]; if relative_starts_with_slash { - format!("{}{relative_uri}", &base_without_last_segment[..base_without_last_segment.len() - 1]) + Ok(format!("{}{relative_uri}", &base_without_last_segment[..base_without_last_segment.len() - 1])) } else { - format!("{base_without_last_segment}{relative_uri}") + Ok(format!("{base_without_last_segment}{relative_uri}")) } } else { - format!("{base_uri}{relative_uri}") + // No path after scheme (e.g., "https://example.com") + // .NET Uri adds a '/' between host and relative URI + if relative_starts_with_slash { + Ok(format!("{base_uri}{relative_uri}")) + } else { + Ok(format!("{base_uri}/{relative_uri}")) + } } } @@ -118,7 +124,8 @@ mod tests { fn test_uri_no_slashes_after_scheme() { let mut parser = Statement::new().unwrap(); let result = parser.parse_and_execute("[uri('https://example.com', 'path')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.compath"); + // .NET Uri behavior: adds a slash between host and relative path + assert_eq!(result, "https://example.com/path"); } #[test] @@ -190,4 +197,13 @@ mod tests { let result = parser.parse_and_execute("[uri('https://example.com/old/path', 'new')]", &Context::new()).unwrap(); assert_eq!(result, "https://example.com/old/new"); } + + #[test] + fn test_uri_empty_base_uri_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('', 'path')]", &Context::new()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("baseUri")); + } } diff --git a/lib/dsc-lib/src/functions/uri_component.rs b/lib/dsc-lib/src/functions/uri_component.rs index 696fb6079..a31fdab8d 100644 --- a/lib/dsc-lib/src/functions/uri_component.rs +++ b/lib/dsc-lib/src/functions/uri_component.rs @@ -27,38 +27,11 @@ impl Function for UriComponent { fn invoke(&self, args: &[Value], _context: &Context) -> Result { let string_to_encode = args[0].as_str().unwrap(); - let result = percent_encode_uri_component(string_to_encode); - Ok(Value::String(result)) + let result = urlencoding::encode(string_to_encode); + Ok(Value::String(result.into_owned())) } } -/// Percent-encodes a string for use as a URI component. -/// -/// Encodes all characters except: -/// - Unreserved characters: A-Z, a-z, 0-9, -, _, ., ~ -/// -/// This follows RFC 3986 for URI component encoding. -fn percent_encode_uri_component(input: &str) -> String { - use std::fmt::Write; - let mut result = String::with_capacity(input.len() * 3); - - for byte in input.bytes() { - match byte { - // Unreserved characters (RFC 3986 section 2.3) - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - result.push(byte as char); - } - // Everything else gets percent-encoded - _ => { - result.push('%'); - let _ = write!(result, "{byte:02X}"); - } - } - } - - result -} - #[cfg(test)] mod tests { use crate::configure::context::Context; diff --git a/lib/dsc-lib/src/functions/uri_component_to_string.rs b/lib/dsc-lib/src/functions/uri_component_to_string.rs index b808f05dc..f66cfcfce 100644 --- a/lib/dsc-lib/src/functions/uri_component_to_string.rs +++ b/lib/dsc-lib/src/functions/uri_component_to_string.rs @@ -27,52 +27,14 @@ impl Function for UriComponentToString { fn invoke(&self, args: &[Value], _context: &Context) -> Result { let uri_encoded_string = args[0].as_str().unwrap(); - let result = percent_decode_uri_component(uri_encoded_string)?; - Ok(Value::String(result)) + let result = urlencoding::decode(uri_encoded_string) + .map_err(|e| DscError::Parser( + t!("functions.uriComponentToString.invalidUtf8", error = e).to_string() + ))?; + Ok(Value::String(result.into_owned())) } } -/// Decodes a percent-encoded URI component string. -/// -/// Decodes percent-encoded sequences (e.g., %20 to space, %40 to @). -/// This is the inverse operation of `percent_encode_uri_component`. -fn percent_decode_uri_component(input: &str) -> Result { - let mut result = Vec::new(); - let bytes = input.as_bytes(); - let mut i = 0; - - while i < bytes.len() { - if bytes[i] == b'%' { - // Check if we have at least 2 more characters - if i + 2 >= bytes.len() { - return Err(DscError::Parser( - t!("functions.uriComponentToString.incompleteSequence", position = i).to_string() - )); - } - - // Parse the two hex digits - let hex_str = std::str::from_utf8(&bytes[i + 1..i + 3]) - .map_err(|_| DscError::Parser( - t!("functions.uriComponentToString.invalidHexDigits", position = i).to_string() - ))?; - - let decoded_byte = u8::from_str_radix(hex_str, 16) - .map_err(|_| DscError::Parser( - t!("functions.uriComponentToString.invalidHexValue", hex = hex_str, position = i).to_string() - ))?; - - result.push(decoded_byte); - i += 3; - } else { - result.push(bytes[i]); - i += 1; - } - } - - String::from_utf8(result) - .map_err(|e| DscError::Parser(t!("functions.uriComponentToString.invalidUtf8", error = e).to_string())) -} - #[cfg(test)] mod tests { use crate::configure::context::Context; From 3e4d997bb8bab4a9f9eaecf0f595f450a0a9a811 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 11 Oct 2025 05:32:03 +0200 Subject: [PATCH 07/19] Update tests --- dsc/tests/dsc_functions.tests.ps1 | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 3308b1db5..e61661055 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -865,7 +865,7 @@ Describe 'tests for function expressions' { @{ expression = "[uri('https://example.com/', '/path/file.html')]"; expected = 'https://example.com/path/file.html' } @{ expression = "[uri('https://example.com/api/v1', 'users')]"; expected = 'https://example.com/api/users' } @{ expression = "[uri('https://example.com/api/v1', '/users')]"; expected = 'https://example.com/api/users' } - @{ expression = "[uri('https://example.com', 'path')]"; expected = 'https://example.compath' } + @{ expression = "[uri('https://example.com', 'path')]"; expected = 'https://example.com/path' } @{ expression = "[uri('https://example.com', '/path')]"; expected = 'https://example.com/path' } @{ expression = "[uri('https://api.example.com/v2/resource/', 'item/123')]"; expected = 'https://api.example.com/v2/resource/item/123' } @{ expression = "[uri('https://example.com/api/', 'search?q=test')]"; expected = 'https://example.com/api/search?q=test' } @@ -891,6 +891,25 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -Be $expected } + It 'uri function error handling: ' -TestCases @( + @{ expression = "[uri('', 'path')]" ; expectedError = 'baseUri parameter cannot be empty' } + ) { + param($expression, $expectedError) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: `"$expression`" +"@ + $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match $expectedError + } + It 'uriComponent function works for: ' -TestCases @( @{ expression = "[uriComponent('hello world')]"; expected = 'hello%20world' } @{ expression = "[uriComponent('hello@example.com')]"; expected = 'hello%40example.com' } From 7924ded7f72da035cc082d90f620648356be7d1a Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 11 Oct 2025 05:37:48 +0200 Subject: [PATCH 08/19] Add test case --- .../reference/schemas/config/functions/uri.md | 50 ++++++++++++++++++- dsc/tests/dsc_functions.tests.ps1 | 2 + lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/functions/uri.rs | 28 +++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/reference/schemas/config/functions/uri.md b/docs/reference/schemas/config/functions/uri.md index f5ab68d9c..2b171ec51 100644 --- a/docs/reference/schemas/config/functions/uri.md +++ b/docs/reference/schemas/config/functions/uri.md @@ -21,6 +21,18 @@ uri(, ) The `uri()` function combines a base URI with a relative URI to create an absolute URI. The function handles trailing and leading slashes intelligently based on the structure of the base URI. +This behavior matches the .NET `Uri` class implementation. + +### Special cases + +- **Empty base URI**: Returns an error. The base URI cannot be empty because an absolute URI + requires a valid base. +- **Protocol-relative URIs (`//host`)**: The relative URI takes the scheme from the base URI. + For example, `uri('https://example.com/', '//foo')` returns `https://foo/`. +- **Triple slash sequences (`///`)**: Returns an error. Three or more consecutive slashes are + invalid URI syntax. + +### Combining behavior The behavior depends on whether the base URI ends with a trailing slash: @@ -29,8 +41,8 @@ The behavior depends on whether the base URI ends with a trailing slash: slashes are combined into one. - **If baseUri doesn't end with a trailing slash**: - - If `baseUri` has no slashes after the scheme (aside from `://` or `//` at the front), the - result is `baseUri` followed by `relativeUri`. + - If `baseUri` has no slashes after the scheme (e.g., `https://example.com`), a slash is + inserted between the host and the relative URI unless the relative URI starts with a slash. - If `baseUri` has slashes after the scheme but doesn't end with a slash, everything from the last slash onward is removed from `baseUri`, and the result is the modified `baseUri` followed by `relativeUri`. @@ -232,6 +244,40 @@ messages: [] hadErrors: false ``` +### Example 6 - Protocol-relative URI + +The following example shows how protocol-relative URIs (starting with `//`) inherit the scheme +from the base URI. + +```yaml +# uri.example.6.dsc.config.yaml +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: Protocol-relative URI + type: Microsoft.DSC.Debug/Echo + properties: + output: + result: "[uri('https://example.com/', '//cdn.example.org/assets')]" + explanation: The relative URI inherits the https scheme from the base +``` + +```bash +dsc config get --file uri.example.6.dsc.config.yaml +``` + +```yaml +results: +- name: Protocol-relative URI + type: Microsoft.DSC.Debug/Echo + result: + actualState: + output: + result: https://cdn.example.org/assets + explanation: The relative URI inherits the https scheme from the base +messages: [] +hadErrors: false +``` + ## Parameters ### baseUri diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index e61661055..2317cb920 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -874,6 +874,7 @@ Describe 'tests for function expressions' { @{ expression = "[uri('https://example.com:8080/', 'api')]"; expected = 'https://example.com:8080/api' } @{ expression = "[uri(concat('https://example.com', '/'), 'path')]"; expected = 'https://example.com/path' } @{ expression = "[uri('//example.com/', 'path')]"; expected = '//example.com/path' } + @{ expression = "[uri('https://example.com/', '//foo')]"; expected = 'https://foo/' } @{ expression = "[uri('https://example.com/a/b/c/', 'd/e/f')]"; expected = 'https://example.com/a/b/c/d/e/f' } @{ expression = "[uri('https://example.com/old/path', 'new')]"; expected = 'https://example.com/old/new' } ) { @@ -893,6 +894,7 @@ Describe 'tests for function expressions' { It 'uri function error handling: ' -TestCases @( @{ expression = "[uri('', 'path')]" ; expectedError = 'baseUri parameter cannot be empty' } + @{ expression = "[uri('https://example.com/', '///foo')]" ; expectedError = 'Invalid URI|invalid sequence' } ) { param($expression, $expectedError) diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index b890f8e1f..d8815c2c7 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -531,6 +531,7 @@ invoked = "uniqueString function" [functions.uri] description = "Creates an absolute URI by combining the baseUri and the relativeUri string" emptyBaseUri = "The baseUri parameter cannot be empty. An absolute URI requires a valid base URI." +invalidRelativeUri = "Invalid URI: The relative URI contains an invalid sequence. Three or more consecutive slashes (///) are not allowed." [functions.uriComponent] description = "Encodes a URI component using percent-encoding" diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index 5da134c9c..4de85735f 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -45,6 +45,18 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { return Ok(base_uri.to_string()); } + if relative_uri.starts_with("///") { + return Err(DscError::Parser(t!("functions.uri.invalidRelativeUri").to_string())); + } + + if relative_uri.starts_with("//") { + if let Some(scheme_end) = base_uri.find("://") { + let scheme = &base_uri[..scheme_end]; + return Ok(format!("{scheme}:{relative_uri}")); + } + return Ok(format!("https:{relative_uri}")); + } + let base_ends_with_slash = base_uri.ends_with('/'); let relative_starts_with_slash = relative_uri.starts_with('/'); @@ -206,4 +218,20 @@ mod tests { let err = result.unwrap_err(); assert!(err.to_string().contains("baseUri")); } + + #[test] + fn test_uri_triple_slash_error() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/', '///foo')]", &Context::new()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("Invalid") || err.to_string().contains("hostname")); + } + + #[test] + fn test_uri_double_slash_protocol_relative() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('https://example.com/', '//foo')]", &Context::new()).unwrap(); + assert_eq!(result, "https://foo/"); + } } From ba58e19057c58d257acfd3e2ea26cea468a3362e Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 11 Oct 2025 06:20:50 +0200 Subject: [PATCH 09/19] Handle // --- lib/dsc-lib/src/functions/uri.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index 4de85735f..a2190a9fb 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -50,11 +50,20 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { } if relative_uri.starts_with("//") { - if let Some(scheme_end) = base_uri.find("://") { - let scheme = &base_uri[..scheme_end]; + // Protocol-relative URI: extract scheme from base + let scheme = if let Some(scheme_end) = base_uri.find("://") { + &base_uri[..scheme_end] + } else { + "https" + }; + + // Check if the protocol-relative URI has a path + let uri_without_slashes = &relative_uri[2..]; // Remove leading // + if uri_without_slashes.contains('/') { + // Has a path, use as-is return Ok(format!("{scheme}:{relative_uri}")); } - return Ok(format!("https:{relative_uri}")); + return Ok(format!("{scheme}:{relative_uri}/")); } let base_ends_with_slash = base_uri.ends_with('/'); From 1f3e404c920fa94e9fc00a82fd339dfe5de7ce27 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 11 Oct 2025 06:30:21 +0200 Subject: [PATCH 10/19] Fix clippy --- lib/dsc-lib/src/functions/uri.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index a2190a9fb..3966e5c35 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -49,7 +49,7 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { return Err(DscError::Parser(t!("functions.uri.invalidRelativeUri").to_string())); } - if relative_uri.starts_with("//") { + if let Some(uri_without_slashes) = relative_uri.strip_prefix("//") { // Protocol-relative URI: extract scheme from base let scheme = if let Some(scheme_end) = base_uri.find("://") { &base_uri[..scheme_end] @@ -58,11 +58,11 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { }; // Check if the protocol-relative URI has a path - let uri_without_slashes = &relative_uri[2..]; // Remove leading // if uri_without_slashes.contains('/') { // Has a path, use as-is return Ok(format!("{scheme}:{relative_uri}")); } + // No path specified, add trailing slash for root return Ok(format!("{scheme}:{relative_uri}/")); } From 1bc53d72fd88505b9dcd5ca482b5a9ce5f538485 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sat, 11 Oct 2025 12:20:23 +0200 Subject: [PATCH 11/19] Removed unused tests --- dsc/tests/dsc_functions.tests.ps1 | 20 -------------------- lib/dsc-lib/locales/en-us.toml | 3 --- 2 files changed, 23 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 2317cb920..fc6bb2993 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -971,24 +971,4 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected } - - It 'uriComponentToString function error handling: ' -TestCases @( - @{ expression = "[uriComponentToString('incomplete%2')]" ; expectedError = 'Invalid percent-encoding: incomplete sequence at position' } - @{ expression = "[uriComponentToString('invalid%ZZ')]" ; expectedError = 'Invalid percent-encoding:.*is not valid hex at position' } - ) { - param($expression, $expectedError) - - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: `"$expression`" -"@ - $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Not -Be 0 - $errorContent = Get-Content $TestDrive/error.log -Raw - $errorContent | Should -Match $expectedError - } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index d8815c2c7..6b60b4988 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -538,9 +538,6 @@ description = "Encodes a URI component using percent-encoding" [functions.uriComponentToString] description = "Returns a string of a URI encoded value" -incompleteSequence = "Invalid percent-encoding: incomplete sequence at position %{position}" -invalidHexDigits = "Invalid percent-encoding: invalid hex digits at position %{position}" -invalidHexValue = "Invalid percent-encoding: '%{hex}' is not valid hex at position %{position}" invalidUtf8 = "Invalid UTF-8 in decoded string: %{error}" [functions.userFunction] From 94bf3472cf6cf3c9ee76309c83bf7fc0307fff9b Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 12 Oct 2025 08:28:50 +0200 Subject: [PATCH 12/19] Fix test cases --- dsc/tests/dsc_functions.tests.ps1 | 59 ++++++++++++++--------- lib/dsc-lib/locales/en-us.toml | 1 + lib/dsc-lib/src/functions/uri.rs | 77 +++++++++++++++++++++---------- 3 files changed, 90 insertions(+), 47 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index fc6bb2993..b02807a9e 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -860,25 +860,29 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -Be $expected } - It 'uri function works for: ' -TestCases @( - @{ expression = "[uri('https://example.com/', 'path/file.html')]"; expected = 'https://example.com/path/file.html' } - @{ expression = "[uri('https://example.com/', '/path/file.html')]"; expected = 'https://example.com/path/file.html' } - @{ expression = "[uri('https://example.com/api/v1', 'users')]"; expected = 'https://example.com/api/users' } - @{ expression = "[uri('https://example.com/api/v1', '/users')]"; expected = 'https://example.com/api/users' } - @{ expression = "[uri('https://example.com', 'path')]"; expected = 'https://example.com/path' } - @{ expression = "[uri('https://example.com', '/path')]"; expected = 'https://example.com/path' } - @{ expression = "[uri('https://api.example.com/v2/resource/', 'item/123')]"; expected = 'https://api.example.com/v2/resource/item/123' } - @{ expression = "[uri('https://example.com/api/', 'search?q=test')]"; expected = 'https://example.com/api/search?q=test' } - @{ expression = "[uri('https://example.com/', '')]"; expected = 'https://example.com/' } - @{ expression = "[uri('http://example.com/', 'page.html')]"; expected = 'http://example.com/page.html' } - @{ expression = "[uri('https://example.com:8080/', 'api')]"; expected = 'https://example.com:8080/api' } - @{ expression = "[uri(concat('https://example.com', '/'), 'path')]"; expected = 'https://example.com/path' } - @{ expression = "[uri('//example.com/', 'path')]"; expected = '//example.com/path' } - @{ expression = "[uri('https://example.com/', '//foo')]"; expected = 'https://foo/' } - @{ expression = "[uri('https://example.com/a/b/c/', 'd/e/f')]"; expected = 'https://example.com/a/b/c/d/e/f' } - @{ expression = "[uri('https://example.com/old/path', 'new')]"; expected = 'https://example.com/old/new' } + It 'uri function works for: + ' -TestCases @( + @{ base = 'https://example.com/'; relative = 'path/file.html' } + @{ base = 'https://example.com/'; relative = '/path/file.html' } + @{ base = 'https://example.com/api/v1'; relative = 'users' } + @{ base = 'https://example.com/api/v1'; relative = '/users' } + @{ base = 'https://example.com'; relative = 'path' } + @{ base = 'https://example.com'; relative = '/path' } + @{ base = 'https://api.example.com/v2/resource/'; relative = 'item/123' } + @{ base = 'https://example.com/api/'; relative = 'search?q=test' } + @{ base = 'https://example.com/'; relative = '' } + @{ base = 'http://example.com/'; relative = 'page.html' } + @{ base = 'https://example.com:8080/'; relative = 'api' } + @{ base = 'https://example.com/'; relative = '//foo' } + @{ base = 'https://example.com/a/b/c/'; relative = 'd/e/f' } + @{ base = 'https://example.com/old/path'; relative = 'new' } ) { - param($expression, $expected) + param($base, $relative) + + $expected = '' + $success = [uri]::TryCreate([uri]$base, [uri]$relative, [ref]$expected) + $success | Should -BeTrue + + $expression = "[uri('$base', '$relative')]" $escapedExpression = $expression -replace "'", "''" $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json @@ -889,15 +893,24 @@ Describe 'tests for function expressions' { output: '$escapedExpression' "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected.AbsoluteUri } - It 'uri function error handling: ' -TestCases @( - @{ expression = "[uri('', 'path')]" ; expectedError = 'baseUri parameter cannot be empty' } - @{ expression = "[uri('https://example.com/', '///foo')]" ; expectedError = 'Invalid URI|invalid sequence' } + It 'uri function error handling: + ' -TestCases @( + @{ base = ''; relative = 'path'; expectedError = 'baseUri parameter cannot be empty' } + @{ base = 'example.com'; relative = 'path'; expectedError = 'baseUri must be an absolute URI' } + @{ base = '/relative/path'; relative = 'file.txt'; expectedError = 'baseUri must be an absolute URI' } + @{ base = 'https://example.com/'; relative = '///foo'; expectedError = 'Invalid URI|invalid sequence' } ) { - param($expression, $expectedError) + param($base, $relative, $expectedError) + + if ($base -ne '') { + $testUri = $null + $dotnetSuccess = [uri]::TryCreate([uri]$base, [uri]$relative, [ref]$testUri) + $dotnetSuccess | Should -BeFalse -Because ".NET Uri.TryCreate should also fail for base='$base' relative='$relative'" + } + $expression = "[uri('$base', '$relative')]" $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 6b60b4988..0e1879384 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -531,6 +531,7 @@ invoked = "uniqueString function" [functions.uri] description = "Creates an absolute URI by combining the baseUri and the relativeUri string" emptyBaseUri = "The baseUri parameter cannot be empty. An absolute URI requires a valid base URI." +notAbsoluteUri = "The baseUri must be an absolute URI (must include a scheme such as http:// or https://)." invalidRelativeUri = "Invalid URI: The relative URI contains an invalid sequence. Three or more consecutive slashes (///) are not allowed." [functions.uriComponent] diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index 3966e5c35..46a2a32ad 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -41,6 +41,11 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { if base_uri.is_empty() { return Err(DscError::Parser(t!("functions.uri.emptyBaseUri").to_string())); } + + if !base_uri.contains("://") { + return Err(DscError::Parser(t!("functions.uri.notAbsoluteUri").to_string())); + } + if relative_uri.is_empty() { return Ok(base_uri.to_string()); } @@ -66,20 +71,9 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { return Ok(format!("{scheme}:{relative_uri}/")); } - let base_ends_with_slash = base_uri.ends_with('/'); let relative_starts_with_slash = relative_uri.starts_with('/'); - // Case 1: baseUri ends with trailing slash - if base_ends_with_slash { - if relative_starts_with_slash { - // Combine trailing and leading slash into one - return Ok(format!("{base_uri}{}", &relative_uri[1..])); - } - return Ok(format!("{base_uri}{relative_uri}")); - } - - // Case 2: baseUri doesn't end with trailing slash - // Check if baseUri has slashes (aside from // near the front) + // Extract scheme and host from base URI let scheme_end = if base_uri.starts_with("http://") || base_uri.starts_with("https://") { base_uri.find("://").map_or(0, |pos| pos + 3) } else if base_uri.starts_with("//") { @@ -88,23 +82,39 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { 0 }; + // If relative URI starts with '/', it replaces the entire path of the base URI + // Keep only scheme://host and append the relative URI + if relative_starts_with_slash { + let after_scheme = &base_uri[scheme_end..]; + if let Some(first_slash_pos) = after_scheme.find('/') { + // Base has a path, extract scheme://host only + let scheme_and_host = &base_uri[..scheme_end + first_slash_pos]; + return Ok(format!("{scheme_and_host}{relative_uri}")); + } + // Base has no path (e.g., "https://example.com"), just append + return Ok(format!("{base_uri}{relative_uri}")); + } + + // Relative URI doesn't start with '/' + // Standard behavior: resolve relative to base + let base_ends_with_slash = base_uri.ends_with('/'); + + if base_ends_with_slash { + // Base ends with '/', just append + return Ok(format!("{base_uri}{relative_uri}")); + } + + // Base doesn't end with '/', need to remove last segment let after_scheme = &base_uri[scheme_end..]; if let Some(last_slash_pos) = after_scheme.rfind('/') { + // Base has a path with segments, remove last segment let base_without_last_segment = &base_uri[..=(scheme_end + last_slash_pos)]; - if relative_starts_with_slash { - Ok(format!("{}{relative_uri}", &base_without_last_segment[..base_without_last_segment.len() - 1])) - } else { - Ok(format!("{base_without_last_segment}{relative_uri}")) - } + Ok(format!("{base_without_last_segment}{relative_uri}")) } else { // No path after scheme (e.g., "https://example.com") - // .NET Uri adds a '/' between host and relative URI - if relative_starts_with_slash { - Ok(format!("{base_uri}{relative_uri}")) - } else { - Ok(format!("{base_uri}/{relative_uri}")) - } + // Add a '/' between host and relative URI + Ok(format!("{base_uri}/{relative_uri}")) } } @@ -138,7 +148,8 @@ mod tests { fn test_uri_no_trailing_slash_with_leading_slash() { let mut parser = Statement::new().unwrap(); let result = parser.parse_and_execute("[uri('https://example.com/api/v1', '/users')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/api/users"); + // When relative starts with '/', it replaces the entire path + assert_eq!(result, "https://example.com/users"); } #[test] @@ -243,4 +254,22 @@ mod tests { let result = parser.parse_and_execute("[uri('https://example.com/', '//foo')]", &Context::new()).unwrap(); assert_eq!(result, "https://foo/"); } + + #[test] + fn test_uri_not_absolute_no_scheme() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('example.com', 'path')]", &Context::new()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("absolute")); + } + + #[test] + fn test_uri_not_absolute_relative_path() { + let mut parser = Statement::new().unwrap(); + let result = parser.parse_and_execute("[uri('/relative/path', 'file.txt')]", &Context::new()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.to_string().contains("absolute")); + } } From 7c85e43d561f36354cd8f5f1cd1a65d75800ba4a Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 12 Oct 2025 08:36:45 +0200 Subject: [PATCH 13/19] Fix test --- lib/dsc-lib/src/functions/uri.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs index 46a2a32ad..3f143faed 100644 --- a/lib/dsc-lib/src/functions/uri.rs +++ b/lib/dsc-lib/src/functions/uri.rs @@ -42,7 +42,7 @@ fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { return Err(DscError::Parser(t!("functions.uri.emptyBaseUri").to_string())); } - if !base_uri.contains("://") { + if !base_uri.contains("://") && !base_uri.starts_with("//") { return Err(DscError::Parser(t!("functions.uri.notAbsoluteUri").to_string())); } From 45e7a249b5dbc2ea052914def61d077b45700a28 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Mon, 13 Oct 2025 04:59:01 +0200 Subject: [PATCH 14/19] Fix test --- dsc/tests/dsc_functions.tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 8d72af636..c032c1d32 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -1027,3 +1027,4 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected } +} \ No newline at end of file From 692b4a2d2f31776debe6a8e9883d5d5df53e91fd Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 14 Oct 2025 04:11:42 +0200 Subject: [PATCH 15/19] Remove `uri()` function --- .../reference/schemas/config/functions/uri.md | 329 ------------------ dsc/tests/dsc_functions.tests.ps1 | 65 ---- lib/dsc-lib/locales/en-us.toml | 6 - lib/dsc-lib/src/functions/mod.rs | 2 - lib/dsc-lib/src/functions/uri.rs | 275 --------------- 5 files changed, 677 deletions(-) delete mode 100644 docs/reference/schemas/config/functions/uri.md delete mode 100644 lib/dsc-lib/src/functions/uri.rs diff --git a/docs/reference/schemas/config/functions/uri.md b/docs/reference/schemas/config/functions/uri.md deleted file mode 100644 index 2b171ec51..000000000 --- a/docs/reference/schemas/config/functions/uri.md +++ /dev/null @@ -1,329 +0,0 @@ ---- -description: Reference for the 'uri' DSC configuration document function -ms.date: 01/10/2025 -ms.topic: reference -title: uri ---- - -# uri - -## Synopsis - -Creates an absolute URI by combining the baseUri and the relativeUri string. - -## Syntax - -```Syntax -uri(, ) -``` - -## Description - -The `uri()` function combines a base URI with a relative URI to create an absolute URI. The -function handles trailing and leading slashes intelligently based on the structure of the base URI. -This behavior matches the .NET `Uri` class implementation. - -### Special cases - -- **Empty base URI**: Returns an error. The base URI cannot be empty because an absolute URI - requires a valid base. -- **Protocol-relative URIs (`//host`)**: The relative URI takes the scheme from the base URI. - For example, `uri('https://example.com/', '//foo')` returns `https://foo/`. -- **Triple slash sequences (`///`)**: Returns an error. Three or more consecutive slashes are - invalid URI syntax. - -### Combining behavior - -The behavior depends on whether the base URI ends with a trailing slash: - -- **If baseUri ends with a trailing slash (`/`)**: The result is `baseUri` followed by - `relativeUri`. If `relativeUri` also begins with a leading slash, the trailing and leading - slashes are combined into one. - -- **If baseUri doesn't end with a trailing slash**: - - If `baseUri` has no slashes after the scheme (e.g., `https://example.com`), a slash is - inserted between the host and the relative URI unless the relative URI starts with a slash. - - If `baseUri` has slashes after the scheme but doesn't end with a slash, everything from the - last slash onward is removed from `baseUri`, and the result is the modified `baseUri` followed - by `relativeUri`. - -Use this function to build API endpoints, file paths, or resource URLs dynamically from -configuration parameters. - -## Examples - -### Example 1 - Build API endpoint with trailing slash - -The following example combines a base API URL ending with a slash with a relative path. - -```yaml -# uri.example.1.dsc.config.yaml -$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -parameters: - apiBase: - type: string - defaultValue: https://api.example.com/v1/ - resourcePath: - type: string - defaultValue: users/123 -resources: -- name: Build API endpoint - type: Microsoft.DSC.Debug/Echo - properties: - output: - endpoint: "[uri(parameters('apiBase'), parameters('resourcePath'))]" -``` - -```bash -dsc config get --file uri.example.1.dsc.config.yaml -``` - -```yaml -results: -- name: Build API endpoint - type: Microsoft.DSC.Debug/Echo - result: - actualState: - output: - endpoint: https://api.example.com/v1/users/123 -messages: [] -hadErrors: false -``` - -### Example 2 - Handle duplicate slashes - -The following example shows how the function automatically handles cases where both the base URI -ends with a slash and the relative URI begins with a slash. - -```yaml -# uri.example.2.dsc.config.yaml -$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -resources: -- name: Combine URIs with duplicate slashes - type: Microsoft.DSC.Debug/Echo - properties: - output: - withDuplicateSlashes: "[uri('https://example.com/', '/api/data')]" - result: The function combines the slashes into one -``` - -```bash -dsc config get --file uri.example.2.dsc.config.yaml -``` - -```yaml -results: -- name: Combine URIs with duplicate slashes - type: Microsoft.DSC.Debug/Echo - result: - actualState: - output: - withDuplicateSlashes: https://example.com/api/data - result: The function combines the slashes into one -messages: [] -hadErrors: false -``` - -### Example 3 - Replace path segments - -The following example demonstrates how `uri()` replaces the last path segment when the base URI -doesn't end with a trailing slash. It uses the [`concat()`][01] function to build the base URL. - -```yaml -# uri.example.3.dsc.config.yaml -$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -parameters: - currentVersion: - type: string - defaultValue: v1 - newVersion: - type: string - defaultValue: v2 -resources: -- name: Update API version - type: Microsoft.DSC.Debug/Echo - properties: - output: - oldEndpoint: "[concat('https://api.example.com/', parameters('currentVersion'))]" - newEndpoint: "[uri(concat('https://api.example.com/', parameters('currentVersion')), parameters('newVersion'))]" -``` - -```bash -dsc config get --file uri.example.3.dsc.config.yaml -``` - -```yaml -results: -- name: Update API version - type: Microsoft.DSC.Debug/Echo - result: - actualState: - output: - oldEndpoint: https://api.example.com/v1 - newEndpoint: https://api.example.com/v2 -messages: [] -hadErrors: false -``` - -### Example 4 - Build resource URLs - -The following example shows how to use `uri()` with [`concat()`][01] to build complete resource -URLs from configuration parameters. - -```yaml -# uri.example.4.dsc.config.yaml -$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -parameters: - storageAccount: - type: string - defaultValue: mystorageaccount - containerName: - type: string - defaultValue: documents - blobName: - type: string - defaultValue: report.pdf -resources: -- name: Build blob URL - type: Microsoft.DSC.Debug/Echo - properties: - output: - blobUrl: >- - [uri( - concat('https://', parameters('storageAccount'), '.blob.core.windows.net/'), - concat(parameters('containerName'), '/', parameters('blobName')) - )] -``` - -```bash -dsc config get --file uri.example.4.dsc.config.yaml -``` - -```yaml -results: -- name: Build blob URL - type: Microsoft.DSC.Debug/Echo - result: - actualState: - output: - blobUrl: https://mystorageaccount.blob.core.windows.net/documents/report.pdf -messages: [] -hadErrors: false -``` - -### Example 5 - Handle query strings and ports - -The following example demonstrates that `uri()` preserves query strings and port numbers correctly. - -```yaml -# uri.example.5.dsc.config.yaml -$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -resources: -- name: URI with special components - type: Microsoft.DSC.Debug/Echo - properties: - output: - withPort: "[uri('https://example.com:8080/', 'api')]" - withQuery: "[uri('https://example.com/api/', 'search?q=test&limit=10')]" -``` - -```bash -dsc config get --file uri.example.5.dsc.config.yaml -``` - -```yaml -results: -- name: URI with special components - type: Microsoft.DSC.Debug/Echo - result: - actualState: - output: - withPort: https://example.com:8080/api - withQuery: https://example.com/api/search?q=test&limit=10 -messages: [] -hadErrors: false -``` - -### Example 6 - Protocol-relative URI - -The following example shows how protocol-relative URIs (starting with `//`) inherit the scheme -from the base URI. - -```yaml -# uri.example.6.dsc.config.yaml -$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -resources: -- name: Protocol-relative URI - type: Microsoft.DSC.Debug/Echo - properties: - output: - result: "[uri('https://example.com/', '//cdn.example.org/assets')]" - explanation: The relative URI inherits the https scheme from the base -``` - -```bash -dsc config get --file uri.example.6.dsc.config.yaml -``` - -```yaml -results: -- name: Protocol-relative URI - type: Microsoft.DSC.Debug/Echo - result: - actualState: - output: - result: https://cdn.example.org/assets - explanation: The relative URI inherits the https scheme from the base -messages: [] -hadErrors: false -``` - -## Parameters - -### baseUri - -The base URI string. The function's behavior depends on whether this value ends with a trailing -slash. - -```yaml -Type: string -Required: true -Position: 1 -``` - -### relativeUri - -The relative URI string to add to the base URI string. This is combined with the base URI according -to the rules described in the Description section. - -```yaml -Type: string -Required: true -Position: 2 -``` - -## Output - -The `uri()` function returns a string containing the absolute URI created by combining the base URI -and relative URI according to the slash-handling rules. - -```yaml -Type: string -``` - -## Related functions - -- [`concat()`][01] - Concatenates multiple strings together -- [`format()`][02] - Creates a formatted string from a template -- [`substring()`][03] - Extracts a portion of a string -- [`replace()`][04] - Replaces text in a string -- [`split()`][05] - Splits a string into an array -- [`parameters()`][06] - Retrieves parameter values - - -[01]: ./concat.md -[02]: ./format.md -[03]: ./substring.md -[04]: ./replace.md -[05]: ./split.md -[06]: ./parameters.md diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index c032c1d32..bd1e7acd3 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -903,71 +903,6 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -Be $expected } - It 'uri function works for: + ' -TestCases @( - @{ base = 'https://example.com/'; relative = 'path/file.html' } - @{ base = 'https://example.com/'; relative = '/path/file.html' } - @{ base = 'https://example.com/api/v1'; relative = 'users' } - @{ base = 'https://example.com/api/v1'; relative = '/users' } - @{ base = 'https://example.com'; relative = 'path' } - @{ base = 'https://example.com'; relative = '/path' } - @{ base = 'https://api.example.com/v2/resource/'; relative = 'item/123' } - @{ base = 'https://example.com/api/'; relative = 'search?q=test' } - @{ base = 'https://example.com/'; relative = '' } - @{ base = 'http://example.com/'; relative = 'page.html' } - @{ base = 'https://example.com:8080/'; relative = 'api' } - @{ base = 'https://example.com/'; relative = '//foo' } - @{ base = 'https://example.com/a/b/c/'; relative = 'd/e/f' } - @{ base = 'https://example.com/old/path'; relative = 'new' } - ) { - param($base, $relative) - - $expected = '' - $success = [uri]::TryCreate([uri]$base, [uri]$relative, [ref]$expected) - $success | Should -BeTrue - - $expression = "[uri('$base', '$relative')]" - $escapedExpression = $expression -replace "'", "''" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: '$escapedExpression' -"@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -BeExactly $expected.AbsoluteUri - } - - It 'uri function error handling: + ' -TestCases @( - @{ base = ''; relative = 'path'; expectedError = 'baseUri parameter cannot be empty' } - @{ base = 'example.com'; relative = 'path'; expectedError = 'baseUri must be an absolute URI' } - @{ base = '/relative/path'; relative = 'file.txt'; expectedError = 'baseUri must be an absolute URI' } - @{ base = 'https://example.com/'; relative = '///foo'; expectedError = 'Invalid URI|invalid sequence' } - ) { - param($base, $relative, $expectedError) - - if ($base -ne '') { - $testUri = $null - $dotnetSuccess = [uri]::TryCreate([uri]$base, [uri]$relative, [ref]$testUri) - $dotnetSuccess | Should -BeFalse -Because ".NET Uri.TryCreate should also fail for base='$base' relative='$relative'" - } - - $expression = "[uri('$base', '$relative')]" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: `"$expression`" -"@ - $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Not -Be 0 - $errorContent = Get-Content $TestDrive/error.log -Raw - $errorContent | Should -Match $expectedError - } - It 'uriComponent function works for: ' -TestCases @( @{ expression = "[uriComponent('hello world')]"; expected = 'hello%20world' } @{ expression = "[uriComponent('hello@example.com')]"; expected = 'hello%40example.com' } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index ef4644047..0b7ba1fb7 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -532,12 +532,6 @@ invalidArgType = "All arguments must either be arrays or objects" description = "Returns a deterministic unique string from the given strings" invoked = "uniqueString function" -[functions.uri] -description = "Creates an absolute URI by combining the baseUri and the relativeUri string" -emptyBaseUri = "The baseUri parameter cannot be empty. An absolute URI requires a valid base URI." -notAbsoluteUri = "The baseUri must be an absolute URI (must include a scheme such as https:// or file://)." -invalidRelativeUri = "Invalid URI: The relative URI contains an invalid sequence. Three or more consecutive slashes (///) are not allowed." - [functions.uriComponent] description = "Encodes a URI component using percent-encoding" diff --git a/lib/dsc-lib/src/functions/mod.rs b/lib/dsc-lib/src/functions/mod.rs index 2a1f49803..1569da00f 100644 --- a/lib/dsc-lib/src/functions/mod.rs +++ b/lib/dsc-lib/src/functions/mod.rs @@ -70,7 +70,6 @@ pub mod trim; pub mod r#true; pub mod union; pub mod unique_string; -pub mod uri; pub mod uri_component; pub mod uri_component_to_string; pub mod user_function; @@ -196,7 +195,6 @@ impl FunctionDispatcher { Box::new(r#true::True{}), Box::new(union::Union{}), Box::new(unique_string::UniqueString{}), - Box::new(uri::Uri{}), Box::new(uri_component::UriComponent{}), Box::new(uri_component_to_string::UriComponentToString{}), Box::new(utc_now::UtcNow{}), diff --git a/lib/dsc-lib/src/functions/uri.rs b/lib/dsc-lib/src/functions/uri.rs deleted file mode 100644 index 3f143faed..000000000 --- a/lib/dsc-lib/src/functions/uri.rs +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::DscError; -use crate::configure::context::Context; -use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; -use rust_i18n::t; -use serde_json::Value; -use super::Function; - -#[derive(Debug, Default)] -pub struct Uri {} - -impl Function for Uri { - fn get_metadata(&self) -> FunctionMetadata { - FunctionMetadata { - name: "uri".to_string(), - description: t!("functions.uri.description").to_string(), - category: vec![FunctionCategory::String], - min_args: 2, - max_args: 2, - accepted_arg_ordered_types: vec![ - vec![FunctionArgKind::String], - vec![FunctionArgKind::String], - ], - remaining_arg_accepted_types: None, - return_types: vec![FunctionArgKind::String], - } - } - - fn invoke(&self, args: &[Value], _context: &Context) -> Result { - let base_uri = args[0].as_str().unwrap(); - let relative_uri = args[1].as_str().unwrap(); - - let result = combine_uri(base_uri, relative_uri)?; - Ok(Value::String(result)) - } -} - -fn combine_uri(base_uri: &str, relative_uri: &str) -> Result { - if base_uri.is_empty() { - return Err(DscError::Parser(t!("functions.uri.emptyBaseUri").to_string())); - } - - if !base_uri.contains("://") && !base_uri.starts_with("//") { - return Err(DscError::Parser(t!("functions.uri.notAbsoluteUri").to_string())); - } - - if relative_uri.is_empty() { - return Ok(base_uri.to_string()); - } - - if relative_uri.starts_with("///") { - return Err(DscError::Parser(t!("functions.uri.invalidRelativeUri").to_string())); - } - - if let Some(uri_without_slashes) = relative_uri.strip_prefix("//") { - // Protocol-relative URI: extract scheme from base - let scheme = if let Some(scheme_end) = base_uri.find("://") { - &base_uri[..scheme_end] - } else { - "https" - }; - - // Check if the protocol-relative URI has a path - if uri_without_slashes.contains('/') { - // Has a path, use as-is - return Ok(format!("{scheme}:{relative_uri}")); - } - // No path specified, add trailing slash for root - return Ok(format!("{scheme}:{relative_uri}/")); - } - - let relative_starts_with_slash = relative_uri.starts_with('/'); - - // Extract scheme and host from base URI - let scheme_end = if base_uri.starts_with("http://") || base_uri.starts_with("https://") { - base_uri.find("://").map_or(0, |pos| pos + 3) - } else if base_uri.starts_with("//") { - 2 - } else { - 0 - }; - - // If relative URI starts with '/', it replaces the entire path of the base URI - // Keep only scheme://host and append the relative URI - if relative_starts_with_slash { - let after_scheme = &base_uri[scheme_end..]; - if let Some(first_slash_pos) = after_scheme.find('/') { - // Base has a path, extract scheme://host only - let scheme_and_host = &base_uri[..scheme_end + first_slash_pos]; - return Ok(format!("{scheme_and_host}{relative_uri}")); - } - // Base has no path (e.g., "https://example.com"), just append - return Ok(format!("{base_uri}{relative_uri}")); - } - - // Relative URI doesn't start with '/' - // Standard behavior: resolve relative to base - let base_ends_with_slash = base_uri.ends_with('/'); - - if base_ends_with_slash { - // Base ends with '/', just append - return Ok(format!("{base_uri}{relative_uri}")); - } - - // Base doesn't end with '/', need to remove last segment - let after_scheme = &base_uri[scheme_end..]; - - if let Some(last_slash_pos) = after_scheme.rfind('/') { - // Base has a path with segments, remove last segment - let base_without_last_segment = &base_uri[..=(scheme_end + last_slash_pos)]; - Ok(format!("{base_without_last_segment}{relative_uri}")) - } else { - // No path after scheme (e.g., "https://example.com") - // Add a '/' between host and relative URI - Ok(format!("{base_uri}/{relative_uri}")) - } -} - -#[cfg(test)] -mod tests { - use crate::configure::context::Context; - use crate::parser::Statement; - - #[test] - fn test_uri_basic_trailing_slash() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/', 'path/file.html')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/path/file.html"); - } - - #[test] - fn test_uri_trailing_and_leading_slash() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/', '/path/file.html')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/path/file.html"); - } - - #[test] - fn test_uri_no_trailing_slash_with_path() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/api/v1', 'users')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/api/users"); - } - - #[test] - fn test_uri_no_trailing_slash_with_leading_slash() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/api/v1', '/users')]", &Context::new()).unwrap(); - // When relative starts with '/', it replaces the entire path - assert_eq!(result, "https://example.com/users"); - } - - #[test] - fn test_uri_no_slashes_after_scheme() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com', 'path')]", &Context::new()).unwrap(); - // .NET Uri behavior: adds a slash between host and relative path - assert_eq!(result, "https://example.com/path"); - } - - #[test] - fn test_uri_no_slashes_after_scheme_with_leading_slash() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com', '/path')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/path"); - } - - #[test] - fn test_uri_complex_path() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://api.example.com/v2/resource/', 'item/123')]", &Context::new()).unwrap(); - assert_eq!(result, "https://api.example.com/v2/resource/item/123"); - } - - #[test] - fn test_uri_query_string() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/api/', 'search?q=test')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/api/search?q=test"); - } - - #[test] - fn test_uri_empty_relative() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/', '')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/"); - } - - #[test] - fn test_uri_http_scheme() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('http://example.com/', 'page.html')]", &Context::new()).unwrap(); - assert_eq!(result, "http://example.com/page.html"); - } - - #[test] - fn test_uri_with_port() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com:8080/', 'api')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com:8080/api"); - } - - #[test] - fn test_uri_nested_function() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri(concat('https://example.com', '/'), 'path')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/path"); - } - - #[test] - fn test_uri_relative_protocol() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('//example.com/', 'path')]", &Context::new()).unwrap(); - assert_eq!(result, "//example.com/path"); - } - - #[test] - fn test_uri_multiple_path_segments() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/a/b/c/', 'd/e/f')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/a/b/c/d/e/f"); - } - - #[test] - fn test_uri_replace_last_segment() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/old/path', 'new')]", &Context::new()).unwrap(); - assert_eq!(result, "https://example.com/old/new"); - } - - #[test] - fn test_uri_empty_base_uri_error() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('', 'path')]", &Context::new()); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(err.to_string().contains("baseUri")); - } - - #[test] - fn test_uri_triple_slash_error() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/', '///foo')]", &Context::new()); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(err.to_string().contains("Invalid") || err.to_string().contains("hostname")); - } - - #[test] - fn test_uri_double_slash_protocol_relative() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('https://example.com/', '//foo')]", &Context::new()).unwrap(); - assert_eq!(result, "https://foo/"); - } - - #[test] - fn test_uri_not_absolute_no_scheme() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('example.com', 'path')]", &Context::new()); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(err.to_string().contains("absolute")); - } - - #[test] - fn test_uri_not_absolute_relative_path() { - let mut parser = Statement::new().unwrap(); - let result = parser.parse_and_execute("[uri('/relative/path', 'file.txt')]", &Context::new()); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(err.to_string().contains("absolute")); - } -} From f86686d689f5e12478f87a8890bfdc9f4f9f3be3 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Tue, 14 Oct 2025 19:49:29 +0200 Subject: [PATCH 16/19] Update tests --- dsc/tests/dsc_functions.tests.ps1 | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index bd1e7acd3..9f6926c4d 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -920,14 +920,13 @@ Describe 'tests for function expressions' { @{ expression = "[uriComponent(' ')]"; expected = '%20' } ) { param($expression, $expected) - $escapedExpression = $expression -replace "'", "''" $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: '$escapedExpression' + output: "$expression" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected @@ -949,15 +948,13 @@ Describe 'tests for function expressions' { @{ expression = "[uriComponentToString(concat('hello', '%20', 'world'))]"; expected = 'hello world' } ) { param($expression, $expected) - - $escapedExpression = $expression -replace "'", "''" $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: '$escapedExpression' + output: "$expression" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -Be $expected From d7a3c4fa5a751545651464653b046adfa11d69af Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Wed, 15 Oct 2025 06:40:27 +0200 Subject: [PATCH 17/19] Use .NET escape methods --- dsc/tests/dsc_functions.tests.ps1 | 1768 +++++++++++++++-------------- 1 file changed, 920 insertions(+), 848 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 9f6926c4d..995306abf 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -2,961 +2,1033 @@ # Licensed under the MIT License. Describe 'tests for function expressions' { - It 'function works: ' -TestCases @( - @{ text = "[concat('a', 'b')]"; expected = 'ab' } - @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } - @{ text = "[concat('a', concat('b', 'c'))]"; expected = 'abc' } - @{ text = "[base64('ab')]"; expected = 'YWI=' } - @{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' } - @{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' } - ) { - param($text, $expected) +# It 'function works: ' -TestCases @( +# @{ text = "[concat('a', 'b')]"; expected = 'ab' } +# @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } +# @{ text = "[concat('a', concat('b', 'c'))]"; expected = 'abc' } +# @{ text = "[base64('ab')]"; expected = 'YWI=' } +# @{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' } +# @{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' } +# ) { +# param($text, $expected) - $escapedText = $text -replace "'", "''" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: '$escapedText' -"@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected - } +# $escapedText = $text -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedText' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } - It 'path() works' -TestCases @( - @{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" } - @{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" } - ) { - param($path, $expected) +# It 'path() works' -TestCases @( +# @{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" } +# @{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" } +# ) { +# param($path, $expected) - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "[path($path)]" -"@ - $out = $config_yaml | dsc config --system-root $PSHOME get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -BeExactly $expected - } +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "[path($path)]" +# "@ +# $out = $config_yaml | dsc config --system-root $PSHOME get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -BeExactly $expected +# } - It 'default systemRoot() is correct for the OS' { - $config_yaml = @' - $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "[systemRoot()]" -'@ - - $expected = if ($IsWindows) { - $env:SYSTEMDRIVE + '\' - } else { - '/' - } - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.results[0].result.actualState.output | Should -BeExactly $expected - } +# It 'default systemRoot() is correct for the OS' { +# $config_yaml = @' +# $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "[systemRoot()]" +# '@ - It 'union function works for: ' -TestCases @( - @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } - @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } - @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } - @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } - @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } - ) { - param($expression, $expected, $isError) +# $expected = if ($IsWindows) { +# $env:SYSTEMDRIVE + '\' +# } else { +# '/' +# } +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 +# $out.results[0].result.actualState.output | Should -BeExactly $expected +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - parameters: - firstObject: - type: object - defaultValue: - one: a - two: b - secondObject: - type: object - defaultValue: - two: c - three: d - firstArray: - type: array - defaultValue: - - ab - - cd - secondArray: - type: array - defaultValue: - - cd - - ef - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - if ($isError) { - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' - } else { - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } - } +# It 'union function works for: ' -TestCases @( +# @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } +# @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } +# @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } +# @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } +# @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } +# ) { +# param($expression, $expected, $isError) - It 'intersection function works for: ' -TestCases @( - @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') } - @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } } - @{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') } - @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } - @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } - @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } - @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } - @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } - @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ - shared = [pscustomobject]@{ value = 42; flag = $true } - level = 1 - } - } - @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } - @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } - ) { - param($expression, $expected, $isError) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# parameters: +# firstObject: +# type: object +# defaultValue: +# one: a +# two: b +# secondObject: +# type: object +# defaultValue: +# two: c +# three: d +# firstArray: +# type: array +# defaultValue: +# - ab +# - cd +# secondArray: +# type: array +# defaultValue: +# - cd +# - ef +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# if ($isError) { +# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) +# (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' +# } else { +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - parameters: - firstObject: - type: object - defaultValue: - one: a - two: b - secondObject: - type: object - defaultValue: - two: b - three: d - thirdObject: - type: object - defaultValue: - two: c - three: d - fourthObject: - type: object - defaultValue: - three: d - four: e - sixthObject: - type: object - defaultValue: - two: b - five: f - nestedObject1: - type: object - defaultValue: - shared: - value: 42 - flag: true - level: 1 - unique1: test - nestedObject2: - type: object - defaultValue: - shared: - value: 42 - flag: true - level: 1 - unique2: test - nestedObject3: - type: object - defaultValue: - shared: - value: 24 - flag: true - level: 1 - unique3: test - nestedObject4: - type: object - defaultValue: - level: 1 - different: - value: 100 - flag: false - firstArray: - type: array - defaultValue: - - ab - - cd - secondArray: - type: array - defaultValue: - - cd - - ef - thirdArray: - type: array - defaultValue: - - ef - - gh - fourthArray: - type: array - defaultValue: - - gh - - ef - - ij - fifthArray: - type: array - defaultValue: - - cd - - kl - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - if ($isError) { - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' - } else { - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } - } +# It 'intersection function works for: ' -TestCases @( +# @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') } +# @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } } +# @{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') } +# @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } +# @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } +# @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } +# @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } +# @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } +# @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ +# shared = [pscustomobject]@{ value = 42; flag = $true } +# level = 1 +# } +# } +# @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } +# @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } +# ) { +# param($expression, $expected, $isError) - It 'contain function works for: ' -TestCases @( - @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } - @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } - @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } - @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } - @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } - @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } - @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } - @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } - @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } - @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } - @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } - @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } - ) { - param($expression, $expected, $isError) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# parameters: +# firstObject: +# type: object +# defaultValue: +# one: a +# two: b +# secondObject: +# type: object +# defaultValue: +# two: b +# three: d +# thirdObject: +# type: object +# defaultValue: +# two: c +# three: d +# fourthObject: +# type: object +# defaultValue: +# three: d +# four: e +# sixthObject: +# type: object +# defaultValue: +# two: b +# five: f +# nestedObject1: +# type: object +# defaultValue: +# shared: +# value: 42 +# flag: true +# level: 1 +# unique1: test +# nestedObject2: +# type: object +# defaultValue: +# shared: +# value: 42 +# flag: true +# level: 1 +# unique2: test +# nestedObject3: +# type: object +# defaultValue: +# shared: +# value: 24 +# flag: true +# level: 1 +# unique3: test +# nestedObject4: +# type: object +# defaultValue: +# level: 1 +# different: +# value: 100 +# flag: false +# firstArray: +# type: array +# defaultValue: +# - ab +# - cd +# secondArray: +# type: array +# defaultValue: +# - cd +# - ef +# thirdArray: +# type: array +# defaultValue: +# - ef +# - gh +# fourthArray: +# type: array +# defaultValue: +# - gh +# - ef +# - ij +# fifthArray: +# type: array +# defaultValue: +# - cd +# - kl +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# if ($isError) { +# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) +# (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' +# } else { +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - parameters: - array: - type: array - defaultValue: - - a - - b - - 0 - - 1 - object: - type: object - defaultValue: - a: 1 - b: 2 - 3: c - string: - type: string - defaultValue: 'hello 123 world!' - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - if ($isError) { - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - (Get-Content $TestDrive/error.log -Raw) | Should -Match 'accepted types are: String, Number' - } else { - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } - } +# It 'contain function works for: ' -TestCases @( +# @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } +# @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } +# @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } +# @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } +# @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } +# @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } +# @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } +# @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } +# @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } +# @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } +# @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } +# @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } +# ) { +# param($expression, $expected, $isError) - It 'length function works for: ' -TestCases @( - @{ expression = "[length(parameters('array'))]" ; expected = 3 } - @{ expression = "[length(parameters('object'))]" ; expected = 4 } - @{ expression = "[length(parameters('string'))]" ; expected = 12 } - @{ expression = "[length('')]"; expected = 0 } - ) { - param($expression, $expected, $isError) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# parameters: +# array: +# type: array +# defaultValue: +# - a +# - b +# - 0 +# - 1 +# object: +# type: object +# defaultValue: +# a: 1 +# b: 2 +# 3: c +# string: +# type: string +# defaultValue: 'hello 123 world!' +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# if ($isError) { +# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) +# (Get-Content $TestDrive/error.log -Raw) | Should -Match 'accepted types are: String, Number' +# } else { +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - parameters: - array: - type: array - defaultValue: - - a - - b - - c - object: - type: object - defaultValue: - one: a - two: b - three: c - four: d - string: - type: string - defaultValue: 'hello world!' - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'length function works for: ' -TestCases @( +# @{ expression = "[length(parameters('array'))]" ; expected = 3 } +# @{ expression = "[length(parameters('object'))]" ; expected = 4 } +# @{ expression = "[length(parameters('string'))]" ; expected = 12 } +# @{ expression = "[length('')]"; expected = 0 } +# ) { +# param($expression, $expected, $isError) - It 'empty function works for: ' -TestCases @( - @{ expression = "[empty(parameters('array'))]" ; expected = $false } - @{ expression = "[empty(parameters('object'))]" ; expected = $false } - @{ expression = "[empty(parameters('string'))]" ; expected = $false } - @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } - @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } - @{ expression = "[empty('')]" ; expected = $true } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# parameters: +# array: +# type: array +# defaultValue: +# - a +# - b +# - c +# object: +# type: object +# defaultValue: +# one: a +# two: b +# three: c +# four: d +# string: +# type: string +# defaultValue: 'hello world!' +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - parameters: - array: - type: array - defaultValue: - - a - - b - - c - emptyArray: - type: array - defaultValue: [] - object: - type: object - defaultValue: - one: a - two: b - three: c - emptyObject: - type: object - defaultValue: {} - string: - type: string - defaultValue: 'hello world!' - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'empty function works for: ' -TestCases @( +# @{ expression = "[empty(parameters('array'))]" ; expected = $false } +# @{ expression = "[empty(parameters('object'))]" ; expected = $false } +# @{ expression = "[empty(parameters('string'))]" ; expected = $false } +# @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } +# @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } +# @{ expression = "[empty('')]" ; expected = $true } +# ) { +# param($expression, $expected) - It 'utcNow function works for: utcNow()' -TestCases @( - @{ format = $null; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$' } - @{ format = "yyyy-MM-dd"; regex = '^\d{4}-\d{2}-\d{2}$' } - @{ format = "yyyy-MM-ddTHH"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}$' } - @{ format = "yyyy-MM-ddTHHZ"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}Z$' } - @{ format = "MMM dd, yyyy HH"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{2}, \d{4} \d{2}$' } - @{ format = "yy-MMMM-dddd tt H"; regex = '^\d{2}-(January|February|March|April|May|June|July|August|September|October|November|December)-(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday) (AM|PM) \d+$' } - @{ format = "MMM ddd zzz"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (Sun|Mon|Tue|Wed|Thu|Fri|Sat) \+00:00$' } - @{ format = "yy yyyy MM MMM MMMM"; regex = '^\d{2} \d{4} \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (January|February|March|April|May|June|July|August|September|October|November|December)$' } - @{ format = "yyyy-MM-ddTHH:mm:ss"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$' } - ) { - param($format, $regex) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# parameters: +# array: +# type: array +# defaultValue: +# - a +# - b +# - c +# emptyArray: +# type: array +# defaultValue: [] +# object: +# type: object +# defaultValue: +# one: a +# two: b +# three: c +# emptyObject: +# type: object +# defaultValue: {} +# string: +# type: string +# defaultValue: 'hello world!' +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - if ($null -ne $format) { - $format = "'$format'" - } +# It 'utcNow function works for: utcNow()' -TestCases @( +# @{ format = $null; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$' } +# @{ format = "yyyy-MM-dd"; regex = '^\d{4}-\d{2}-\d{2}$' } +# @{ format = "yyyy-MM-ddTHH"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}$' } +# @{ format = "yyyy-MM-ddTHHZ"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}Z$' } +# @{ format = "MMM dd, yyyy HH"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{2}, \d{4} \d{2}$' } +# @{ format = "yy-MMMM-dddd tt H"; regex = '^\d{2}-(January|February|March|April|May|June|July|August|September|October|November|December)-(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday) (AM|PM) \d+$' } +# @{ format = "MMM ddd zzz"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (Sun|Mon|Tue|Wed|Thu|Fri|Sat) \+00:00$' } +# @{ format = "yy yyyy MM MMM MMMM"; regex = '^\d{2} \d{4} \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (January|February|March|April|May|June|July|August|September|October|November|December)$' } +# @{ format = "yyyy-MM-ddTHH:mm:ss"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$' } +# ) { +# param($format, $regex) - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - parameters: - test: - type: string - defaultValue: "[utcNow($format)]" - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "[parameters('test')]" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - # ConvertFrom-Json will convert the date to a DateTime object, so we use regex to capture the string - $out -match '"output":"(?.*?)"' | Should -BeTrue -Because "Output should contain a date" - $actual = $matches['date'] - # compare against the regex - $actual | Should -Match $regex -Because "Output date '$actual' should match regex '$regex'" - } +# if ($null -ne $format) { +# $format = "'$format'" +# } - It 'utcNow errors if used not as a parameter default' { - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "[utcNow()]" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) - $out | Should -BeNullOrEmpty -Because "Output should be null or empty" - (Get-Content $TestDrive/error.log -Raw) | Should -Match "The 'utcNow\(\)' function can only be used as a parameter default" - } +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# parameters: +# test: +# type: string +# defaultValue: "[utcNow($format)]" +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "[parameters('test')]" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# # ConvertFrom-Json will convert the date to a DateTime object, so we use regex to capture the string +# $out -match '"output":"(?.*?)"' | Should -BeTrue -Because "Output should contain a date" +# $actual = $matches['date'] +# # compare against the regex +# $actual | Should -Match $regex -Because "Output date '$actual' should match regex '$regex'" +# } - It 'uniqueString function works for: ' -TestCases @( - @{ expression = "[uniqueString('a')]" ; expected = 'cfvwxu6sc4lqo' } - @{ expression = "[uniqueString('a', 'b', 'c')]" ; expected = 'bhw7m6t6ntwd6' } - @{ expression = "[uniqueString('a', 'b', 'c', 'd')]" ; expected = 'yxzg7ur4qetcy' } - ) { - param($expression, $expected) +# It 'utcNow errors if used not as a parameter default' { +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "[utcNow()]" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) +# $out | Should -BeNullOrEmpty -Because "Output should be null or empty" +# (Get-Content $TestDrive/error.log -Raw) | Should -Match "The 'utcNow\(\)' function can only be used as a parameter default" +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - $out.results[0].result.actualState.output | Should -BeExactly $expected - } +# It 'uniqueString function works for: ' -TestCases @( +# @{ expression = "[uniqueString('a')]" ; expected = 'cfvwxu6sc4lqo' } +# @{ expression = "[uniqueString('a', 'b', 'c')]" ; expected = 'bhw7m6t6ntwd6' } +# @{ expression = "[uniqueString('a', 'b', 'c', 'd')]" ; expected = 'yxzg7ur4qetcy' } +# ) { +# param($expression, $expected) - It 'string function works for: ' -TestCases @( - @{ expression = "[string('hello')]"; expected = 'hello' } - @{ expression = "[string(123)]"; expected = '123' } - @{ expression = "[string(true)]"; expected = 'true' } - @{ expression = "[string(null())]"; expected = 'null' } - @{ expression = "[string(createArray('a', 'b'))]"; expected = '["a","b"]' } - @{ expression = "[string(createObject('a', 1))]"; expected = '{"a":1}' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# $out.results[0].result.actualState.output | Should -BeExactly $expected +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'string function works for: ' -TestCases @( +# @{ expression = "[string('hello')]"; expected = 'hello' } +# @{ expression = "[string(123)]"; expected = '123' } +# @{ expression = "[string(true)]"; expected = 'true' } +# @{ expression = "[string(null())]"; expected = 'null' } +# @{ expression = "[string(createArray('a', 'b'))]"; expected = '["a","b"]' } +# @{ expression = "[string(createObject('a', 1))]"; expected = '{"a":1}' } +# ) { +# param($expression, $expected) - It 'array function works for: ' -TestCases @( - @{ expression = "[array('hello')]"; expected = @('hello') } - @{ expression = "[array(42)]"; expected = @(42) } - @{ expression = "[array(createObject('key', 'value'))]"; expected = @([pscustomobject]@{ key = 'value' }) } - @{ expression = "[array(createArray('a', 'b'))]"; expected = @(@('a', 'b')) } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'array function works for: ' -TestCases @( +# @{ expression = "[array('hello')]"; expected = @('hello') } +# @{ expression = "[array(42)]"; expected = @(42) } +# @{ expression = "[array(createObject('key', 'value'))]"; expected = @([pscustomobject]@{ key = 'value' }) } +# @{ expression = "[array(createArray('a', 'b'))]"; expected = @(@('a', 'b')) } +# ) { +# param($expression, $expected) - It 'first function works for: ' -TestCases @( - @{ expression = "[first(createArray('hello', 'world'))]"; expected = 'hello' } - @{ expression = "[first(createArray(1, 2, 3))]"; expected = 1 } - @{ expression = "[first('hello')]"; expected = 'h' } - @{ expression = "[first('a')]"; expected = 'a' } - @{ expression = "[first(array('mixed'))]"; expected = 'mixed' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'first function works for: ' -TestCases @( +# @{ expression = "[first(createArray('hello', 'world'))]"; expected = 'hello' } +# @{ expression = "[first(createArray(1, 2, 3))]"; expected = 1 } +# @{ expression = "[first('hello')]"; expected = 'h' } +# @{ expression = "[first('a')]"; expected = 'a' } +# @{ expression = "[first(array('mixed'))]"; expected = 'mixed' } +# ) { +# param($expression, $expected) - It 'indexOf function works for: ' -TestCases @( - @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 } - @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 } - @{ expression = "[indexOf(createArray(10, 20, 30), 20)]"; expected = 1 } - @{ expression = "[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 0 } - @{ expression = "[indexOf(createArray('apple', 'banana'), 'orange')]"; expected = -1 } - @{ expression = "[indexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } - @{ expression = "[indexOf(createArray(), 'test')]"; expected = -1 } - @{ expression = "[indexOf(createArray(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]"; expected = 1 } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'indexOf function works for: ' -TestCases @( +# @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 } +# @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 } +# @{ expression = "[indexOf(createArray(10, 20, 30), 20)]"; expected = 1 } +# @{ expression = "[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 0 } +# @{ expression = "[indexOf(createArray('apple', 'banana'), 'orange')]"; expected = -1 } +# @{ expression = "[indexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } +# @{ expression = "[indexOf(createArray(), 'test')]"; expected = -1 } +# @{ expression = "[indexOf(createArray(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]"; expected = 1 } +# ) { +# param($expression, $expected) - It 'join function works for: ' -TestCases @( - @{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' } - @{ expression = "[join(createArray(), '-')]"; expected = '' } - @{ expression = "[join(createArray(1,2,3), ',')]"; expected = '1,2,3' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'join function works for: ' -TestCases @( +# @{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' } +# @{ expression = "[join(createArray(), '-')]"; expected = '' } +# @{ expression = "[join(createArray(1,2,3), ',')]"; expected = '1,2,3' } +# ) { +# param($expression, $expected) - It 'skip function works for: ' -TestCases @( - @{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') } - @{ expression = "[skip('hello', 2)]"; expected = 'llo' } - @{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a', 'b') } - @{ expression = "[skip('abc', 0)]"; expected = 'abc' } - @{ expression = "[skip(createArray('a','b'), 5)]"; expected = @() } - @{ expression = "[skip('', 1)]"; expected = '' } - # Negative counts are treated as zero - @{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x', 'y') } - @{ expression = "[skip('xy', -1)]"; expected = 'xy' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'skip function works for: ' -TestCases @( +# @{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') } +# @{ expression = "[skip('hello', 2)]"; expected = 'llo' } +# @{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a', 'b') } +# @{ expression = "[skip('abc', 0)]"; expected = 'abc' } +# @{ expression = "[skip(createArray('a','b'), 5)]"; expected = @() } +# @{ expression = "[skip('', 1)]"; expected = '' } +# # Negative counts are treated as zero +# @{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x', 'y') } +# @{ expression = "[skip('xy', -1)]"; expected = 'xy' } +# ) { +# param($expression, $expected) - It 'lastIndexOf function works for: ' -TestCases @( - @{ expression = "[lastIndexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 2 } - @{ expression = "[lastIndexOf(createArray(10, 20, 30, 20), 20)]"; expected = 3 } - @{ expression = "[lastIndexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } - @{ expression = "[lastIndexOf(createArray(createArray('a','b'), createArray('c','d'), createArray('a','b')), createArray('a','b'))]"; expected = 2 } - @{ expression = "[lastIndexOf(createArray(createObject('name','John'), createObject('name','Jane'), createObject('name','John')), createObject('name','John'))]"; expected = 2 } - @{ expression = "[lastIndexOf(createArray(), 'test')]"; expected = -1 } - # Objects are compared by deep equality: same keys and values are equal, regardless of property order. - # Both createObject('a',1,'b',2) and createObject('b',2,'a',1) are considered equal. - # Therefore, lastIndexOf returns 1 (the last position where an equal object occurs). - @{ expression = "[lastIndexOf(createArray(createObject('a',1,'b',2), createObject('b',2,'a',1)), createObject('a',1,'b',2))]"; expected = 1 } - @{ expression = "[lastIndexOf(createArray('1','2','3'), 1)]"; expected = -1 } - @{ expression = "[lastIndexOf(createArray(1,2,3), '1')]"; expected = -1 } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'lastIndexOf function works for: ' -TestCases @( +# @{ expression = "[lastIndexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 2 } +# @{ expression = "[lastIndexOf(createArray(10, 20, 30, 20), 20)]"; expected = 3 } +# @{ expression = "[lastIndexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } +# @{ expression = "[lastIndexOf(createArray(createArray('a','b'), createArray('c','d'), createArray('a','b')), createArray('a','b'))]"; expected = 2 } +# @{ expression = "[lastIndexOf(createArray(createObject('name','John'), createObject('name','Jane'), createObject('name','John')), createObject('name','John'))]"; expected = 2 } +# @{ expression = "[lastIndexOf(createArray(), 'test')]"; expected = -1 } +# # Objects are compared by deep equality: same keys and values are equal, regardless of property order. +# # Both createObject('a',1,'b',2) and createObject('b',2,'a',1) are considered equal. +# # Therefore, lastIndexOf returns 1 (the last position where an equal object occurs). +# @{ expression = "[lastIndexOf(createArray(createObject('a',1,'b',2), createObject('b',2,'a',1)), createObject('a',1,'b',2))]"; expected = 1 } +# @{ expression = "[lastIndexOf(createArray('1','2','3'), 1)]"; expected = -1 } +# @{ expression = "[lastIndexOf(createArray(1,2,3), '1')]"; expected = -1 } +# ) { +# param($expression, $expected) - It 'context function works' { - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "[context()]" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - $context = $out.results[0].result.actualState.output - $os = osinfo | ConvertFrom-Json - $context.os.family | Should -BeExactly $os.family - $context.os.version | Should -BeExactly $os.version - $context.os.bitness | Should -BeExactly $os.bitness - $context.os.architecture | Should -BeExactly $os.architecture - $context.security | Should -BeExactly $out.metadata.'Microsoft.DSC'.securityContext - } +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - It 'range function works: ' -TestCases @( - @{ expression = '[range(1, 3)]'; expected = @(1, 2, 3) } - @{ expression = '[range(0, 5)]'; expected = @(0, 1, 2, 3, 4) } - @{ expression = '[range(-2, 4)]'; expected = @(-2, -1, 0, 1) } - @{ expression = '[range(10, 0)]'; expected = @() } - @{ expression = '[range(100, 3)]'; expected = @(100, 101, 102) } - @{ expression = '[first(range(2147473647, 10000))]'; expected = 2147473647 } - ) { - param($expression, $expected) +# It 'context function works' { +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "[context()]" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# $context = $out.results[0].result.actualState.output +# $os = osinfo | ConvertFrom-Json +# $context.os.family | Should -BeExactly $os.family +# $context.os.version | Should -BeExactly $os.version +# $context.os.bitness | Should -BeExactly $os.bitness +# $context.os.architecture | Should -BeExactly $os.architecture +# $context.security | Should -BeExactly $out.metadata.'Microsoft.DSC'.securityContext +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } +# It 'range function works: ' -TestCases @( +# @{ expression = '[range(1, 3)]'; expected = @(1, 2, 3) } +# @{ expression = '[range(0, 5)]'; expected = @(0, 1, 2, 3, 4) } +# @{ expression = '[range(-2, 4)]'; expected = @(-2, -1, 0, 1) } +# @{ expression = '[range(10, 0)]'; expected = @() } +# @{ expression = '[range(100, 3)]'; expected = @(100, 101, 102) } +# @{ expression = '[first(range(2147473647, 10000))]'; expected = 2147473647 } +# ) { +# param($expression, $expected) - It 'range function handles errors correctly: ' -TestCases @( - @{ expression = '[range(1, -1)]'; expectedError = 'Count must be non-negative' } - @{ expression = '[range(1, 10001)]'; expectedError = 'Count must not exceed 10000' } - @{ expression = '[range(2147483647, 1)]'; expectedError = 'Sum of startIndex and count must not exceed 2147483647' } - ) { - param($expression, $expectedError) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Not -Be 0 - $errorContent = Get-Content $TestDrive/error.log -Raw - $errorContent | Should -Match ([regex]::Escape($expectedError)) - } +# It 'range function handles errors correctly: ' -TestCases @( +# @{ expression = '[range(1, -1)]'; expectedError = 'Count must be non-negative' } +# @{ expression = '[range(1, 10001)]'; expectedError = 'Count must not exceed 10000' } +# @{ expression = '[range(2147483647, 1)]'; expectedError = 'Sum of startIndex and count must not exceed 2147483647' } +# ) { +# param($expression, $expectedError) - It 'substring function works for: ' -TestCases @( - @{ expression = "[substring('hello world', 6, 5)]"; expected = 'world' } - @{ expression = "[substring('hello', 0, 2)]"; expected = 'he' } - @{ expression = "[substring('hello', 1, 3)]"; expected = 'ell' } - @{ expression = "[substring('hello', 2)]"; expected = 'llo' } - @{ expression = "[substring('hello', 0)]"; expected = 'hello' } - @{ expression = "[substring('hello', 5)]"; expected = '' } - @{ expression = "[substring('hello', 1, 1)]"; expected = 'e' } - @{ expression = "[substring('hello', 5, 0)]"; expected = '' } - @{ expression = "[substring('', 0)]"; expected = '' } - @{ expression = "[substring('', 0, 0)]"; expected = '' } - @{ expression = "[substring('héllo', 1, 2)]"; expected = 'él' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log +# $LASTEXITCODE | Should -Not -Be 0 +# $errorContent = Get-Content $TestDrive/error.log -Raw +# $errorContent | Should -Match ([regex]::Escape($expectedError)) +# } - $escapedExpression = $expression -replace "'", "''" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: '$escapedExpression' -"@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected - } +# It 'substring function works for: ' -TestCases @( +# @{ expression = "[substring('hello world', 6, 5)]"; expected = 'world' } +# @{ expression = "[substring('hello', 0, 2)]"; expected = 'he' } +# @{ expression = "[substring('hello', 1, 3)]"; expected = 'ell' } +# @{ expression = "[substring('hello', 2)]"; expected = 'llo' } +# @{ expression = "[substring('hello', 0)]"; expected = 'hello' } +# @{ expression = "[substring('hello', 5)]"; expected = '' } +# @{ expression = "[substring('hello', 1, 1)]"; expected = 'e' } +# @{ expression = "[substring('hello', 5, 0)]"; expected = '' } +# @{ expression = "[substring('', 0)]"; expected = '' } +# @{ expression = "[substring('', 0, 0)]"; expected = '' } +# @{ expression = "[substring('héllo', 1, 2)]"; expected = 'él' } +# ) { +# param($expression, $expected) - It 'substring function error handling: ' -TestCases @( - @{ expression = "[substring('hello', -1, 2)]"; expectedError = 'Start index cannot be negative' } - @{ expression = "[substring('hello', 1, -1)]"; expectedError = 'Length cannot be negative' } - @{ expression = "[substring('hello', 10, 1)]"; expectedError = 'Start index is beyond the end of the string' } - @{ expression = "[substring('hello', 2, 10)]"; expectedError = 'Length extends beyond the end of the string' } - ) { - param($expression, $expectedError) +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: `"$expression`" -"@ - $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Not -Be 0 - $errorContent = Get-Content $TestDrive/error.log -Raw - $errorContent | Should -Match ([regex]::Escape($expectedError)) - } +# It 'substring function error handling: ' -TestCases @( +# @{ expression = "[substring('hello', -1, 2)]"; expectedError = 'Start index cannot be negative' } +# @{ expression = "[substring('hello', 1, -1)]"; expectedError = 'Length cannot be negative' } +# @{ expression = "[substring('hello', 10, 1)]"; expectedError = 'Start index is beyond the end of the string' } +# @{ expression = "[substring('hello', 2, 10)]"; expectedError = 'Length extends beyond the end of the string' } +# ) { +# param($expression, $expectedError) - It 'mixed booleans with functions works' -TestCases @( - @{ expression = "[and(true(), false, not(false))]"; expected = $false } - @{ expression = "[or(false, false(), not(false()))]"; expected = $true } - @{ expression = "[and(true(), true, not(false))]"; expected = $true } - @{ expression = "[or(false, false(), not(true()))]"; expected = $false } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: `"$expression`" +# "@ +# $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log +# $LASTEXITCODE | Should -Not -Be 0 +# $errorContent = Get-Content $TestDrive/error.log -Raw +# $errorContent | Should -Match ([regex]::Escape($expectedError)) +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: "$expression" -"@ - $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) - $out.results[0].result.actualState.output | Should -BeExactly $expected - } +# It 'mixed booleans with functions works' -TestCases @( +# @{ expression = "[and(true(), false, not(false))]"; expected = $false } +# @{ expression = "[or(false, false(), not(false()))]"; expected = $true } +# @{ expression = "[and(true(), true, not(false))]"; expected = $true } +# @{ expression = "[or(false, false(), not(true()))]"; expected = $false } +# ) { +# param($expression, $expected) - It 'base64ToString function works for: ' -TestCases @( - @{ expression = "[base64ToString('aGVsbG8gd29ybGQ=')]"; expected = 'hello world' } - @{ expression = "[base64ToString('')]"; expected = '' } - @{ expression = "[base64ToString('aMOpbGxv')]"; expected = 'héllo' } - @{ expression = "[base64ToString('eyJrZXkiOiJ2YWx1ZSJ9')]"; expected = '{"key":"value"}' } - @{ expression = "[base64ToString(base64('test message'))]"; expected = 'test message' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: "$expression" +# "@ +# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json +# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) +# $out.results[0].result.actualState.output | Should -BeExactly $expected +# } - $escapedExpression = $expression -replace "'", "''" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: '$escapedExpression' -"@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected - } +# It 'base64ToString function works for: ' -TestCases @( +# @{ expression = "[base64ToString('aGVsbG8gd29ybGQ=')]"; expected = 'hello world' } +# @{ expression = "[base64ToString('')]"; expected = '' } +# @{ expression = "[base64ToString('aMOpbGxv')]"; expected = 'héllo' } +# @{ expression = "[base64ToString('eyJrZXkiOiJ2YWx1ZSJ9')]"; expected = '{"key":"value"}' } +# @{ expression = "[base64ToString(base64('test message'))]"; expected = 'test message' } +# ) { +# param($expression, $expected) - It 'base64ToString function error handling: ' -TestCases @( - @{ expression = "[base64ToString('invalid!@#')]" ; expectedError = 'Invalid base64 encoding' } - @{ expression = "[base64ToString('/w==')]" ; expectedError = 'Decoded bytes do not form valid UTF-8' } - ) { - param($expression, $expectedError) +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: `"$expression`" -"@ - $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log - $LASTEXITCODE | Should -Not -Be 0 - $errorContent = Get-Content $TestDrive/error.log -Raw - $errorContent | Should -Match $expectedError - } +# It 'base64ToString function error handling: ' -TestCases @( +# @{ expression = "[base64ToString('invalid!@#')]" ; expectedError = 'Invalid base64 encoding' } +# @{ expression = "[base64ToString('/w==')]" ; expectedError = 'Decoded bytes do not form valid UTF-8' } +# ) { +# param($expression, $expectedError) - It 'toUpper function works for: ' -TestCases @( - @{ expression = "[toUpper('hello world')]"; expected = 'HELLO WORLD' } - @{ expression = "[toUpper('Hello World')]"; expected = 'HELLO WORLD' } - @{ expression = "[toUpper('HELLO WORLD')]"; expected = 'HELLO WORLD' } - @{ expression = "[toUpper('')]"; expected = '' } - @{ expression = "[toUpper('Hello123!@#')]"; expected = 'HELLO123!@#' } - @{ expression = "[toUpper('café')]"; expected = 'CAFÉ' } - @{ expression = "[toUpper(' hello world ')]"; expected = ' HELLO WORLD ' } - @{ expression = "[toUpper('a')]"; expected = 'A' } - @{ expression = "[toUpper(concat('hello', ' world'))]"; expected = 'HELLO WORLD' } - ) { - param($expression, $expected) +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: `"$expression`" +# "@ +# $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log +# $LASTEXITCODE | Should -Not -Be 0 +# $errorContent = Get-Content $TestDrive/error.log -Raw +# $errorContent | Should -Match $expectedError +# } - $escapedExpression = $expression -replace "'", "''" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: '$escapedExpression' -"@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected - } +# It 'toUpper function works for: ' -TestCases @( +# @{ expression = "[toUpper('hello world')]"; expected = 'HELLO WORLD' } +# @{ expression = "[toUpper('Hello World')]"; expected = 'HELLO WORLD' } +# @{ expression = "[toUpper('HELLO WORLD')]"; expected = 'HELLO WORLD' } +# @{ expression = "[toUpper('')]"; expected = '' } +# @{ expression = "[toUpper('Hello123!@#')]"; expected = 'HELLO123!@#' } +# @{ expression = "[toUpper('café')]"; expected = 'CAFÉ' } +# @{ expression = "[toUpper(' hello world ')]"; expected = ' HELLO WORLD ' } +# @{ expression = "[toUpper('a')]"; expected = 'A' } +# @{ expression = "[toUpper(concat('hello', ' world'))]"; expected = 'HELLO WORLD' } +# ) { +# param($expression, $expected) - It 'toLower function works for: ' -TestCases @( - @{ expression = "[toLower('HELLO WORLD')]"; expected = 'hello world' } - @{ expression = "[toLower('Hello World')]"; expected = 'hello world' } - @{ expression = "[toLower('hello world')]"; expected = 'hello world' } - @{ expression = "[toLower('')]"; expected = '' } - @{ expression = "[toLower('HELLO123!@#')]"; expected = 'hello123!@#' } - @{ expression = "[toLower('CAFÉ')]"; expected = 'café' } - @{ expression = "[toLower(' HELLO WORLD ')]"; expected = ' hello world ' } - @{ expression = "[toLower('A')]"; expected = 'a' } - @{ expression = "[toLower(concat('HELLO', ' WORLD'))]"; expected = 'hello world' } - ) { - param($expression, $expected) +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } + +# It 'toLower function works for: ' -TestCases @( +# @{ expression = "[toLower('HELLO WORLD')]"; expected = 'hello world' } +# @{ expression = "[toLower('Hello World')]"; expected = 'hello world' } +# @{ expression = "[toLower('hello world')]"; expected = 'hello world' } +# @{ expression = "[toLower('')]"; expected = '' } +# @{ expression = "[toLower('HELLO123!@#')]"; expected = 'hello123!@#' } +# @{ expression = "[toLower('CAFÉ')]"; expected = 'café' } +# @{ expression = "[toLower(' HELLO WORLD ')]"; expected = ' hello world ' } +# @{ expression = "[toLower('A')]"; expected = 'a' } +# @{ expression = "[toLower(concat('HELLO', ' WORLD'))]"; expected = 'hello world' } +# ) { +# param($expression, $expected) + +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } + +# It 'trim function works for: ' -TestCases @( +# @{ expression = "[trim(' hello')]"; expected = 'hello' } +# @{ expression = "[trim('hello ')]"; expected = 'hello' } +# @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } +# @{ expression = "[trim('hello')]"; expected = 'hello' } +# @{ expression = "[trim('')]"; expected = '' } +# @{ expression = "[trim(' ')]"; expected = '' } +# @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } +# @{ expression = "[trim(' café ')]"; expected = 'café' } +# @{ expression = "[trim(' a ')]"; expected = 'a' } +# @{ expression = "[trim(concat(' hello', ' '))]"; expected = 'hello' } +# ) { +# param($expression, $expected) - $escapedExpression = $expression -replace "'", "''" +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } + +# It 'items function converts object to array: ' -TestCases @( +# @{ expression = "[length(items(createObject('a', 1, 'b', 2)))]"; expected = 2 } +# @{ expression = "[length(items(createObject()))]"; expected = 0 } +# @{ expression = "[items(createObject('name', 'John'))[0].key]"; expected = 'name' } +# @{ expression = "[items(createObject('name', 'John'))[0].value]"; expected = 'John' } +# @{ expression = "[items(createObject('a', 1, 'b', 2, 'c', 3))[1].key]"; expected = 'b' } +# @{ expression = "[items(createObject('x', 'hello', 'y', 'world'))[0].value]"; expected = 'hello' } +# ) { +# param($expression, $expected) + +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } + +# It 'items function handles nested values: ' -TestCases @( +# @{ expression = "[items(createObject('person', createObject('name', 'John')))[0].value.name]"; expected = 'John' } +# @{ expression = "[items(createObject('list', createArray('a','b','c')))[0].value[1]]"; expected = 'b' } +# @{ expression = "[length(items(createObject('obj', createObject('x', 1, 'y', 2)))[0].value)]"; expected = 2 } +# ) { +# param($expression, $expected) + +# $escapedExpression = $expression -replace "'", "''" +# $config_yaml = @" +# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +# resources: +# - name: Echo +# type: Microsoft.DSC.Debug/Echo +# properties: +# output: '$escapedExpression' +# "@ +# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json +# $out.results[0].result.actualState.output | Should -Be $expected +# } + + It 'uriComponent function works for: ' -TestCases @( + @{ testInput = 'hello world' } + @{ testInput = 'hello@example.com' } + @{ testInput = 'https://example.com/path?query=value' } + @{ testInput = '' } + @{ testInput = 'ABCabc123-_.~' } + @{ testInput = ':/?#[]@!$&()*+,;=' } + @{ testInput = 'café' } + @{ testInput = 'name=John Doe&age=30' } + @{ testInput = '/path/to/my file.txt' } + @{ testInput = 'user+tag@example.com' } + @{ testInput = '1234567890' } + @{ testInput = '100%' } + @{ testInput = ' ' } + ) { + param($testInput) + + $expected = [Uri]::EscapeDataString($testInput) + $expression = "[uriComponent('$($testInput -replace "'", "''")')]" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: '$escapedExpression' + output: "$expression" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected } - - It 'trim function works for: ' -TestCases @( - @{ expression = "[trim(' hello')]"; expected = 'hello' } - @{ expression = "[trim('hello ')]"; expected = 'hello' } - @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } - @{ expression = "[trim('hello')]"; expected = 'hello' } - @{ expression = "[trim('')]"; expected = '' } - @{ expression = "[trim(' ')]"; expected = '' } - @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } - @{ expression = "[trim(' café ')]"; expected = 'café' } - @{ expression = "[trim(' a ')]"; expected = 'a' } - @{ expression = "[trim(concat(' hello', ' '))]"; expected = 'hello' } - ) { - param($expression, $expected) - - $escapedExpression = $expression -replace "'", "''" + + It 'uriComponent function works with concat' { + $input1 = 'hello' + $input2 = ' ' + $input3 = 'world' + $expected = [Uri]::EscapeDataString($input1 + $input2 + $input3) + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: '$escapedExpression' + output: "[uriComponent(concat('hello', ' ', 'world'))]" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected } - It 'items function converts object to array: ' -TestCases @( - @{ expression = "[length(items(createObject('a', 1, 'b', 2)))]"; expected = 2 } - @{ expression = "[length(items(createObject()))]"; expected = 0 } - @{ expression = "[items(createObject('name', 'John'))[0].key]"; expected = 'name' } - @{ expression = "[items(createObject('name', 'John'))[0].value]"; expected = 'John' } - @{ expression = "[items(createObject('a', 1, 'b', 2, 'c', 3))[1].key]"; expected = 'b' } - @{ expression = "[items(createObject('x', 'hello', 'y', 'world'))[0].value]"; expected = 'hello' } + It 'uriComponentToString function works for: ' -TestCases @( + @{ testInput = 'hello%20world' } + @{ testInput = 'hello%40example.com' } + @{ testInput = 'https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue' } + @{ testInput = '' } + @{ testInput = 'ABCabc123-_.~' } + @{ testInput = '%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D' } + @{ testInput = 'caf%C3%A9' } + @{ testInput = 'name%3DJohn%20Doe%26age%3D30' } + @{ testInput = '%2Fpath%2Fto%2Fmy%20file.txt' } + @{ testInput = '100%25' } ) { - param($expression, $expected) - - $escapedExpression = $expression -replace "'", "''" + param($testInput) + + $expected = [Uri]::UnescapeDataString($testInput) + $expression = "[uriComponentToString('$($testInput -replace "'", "''")')]" + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: '$escapedExpression' + output: "$expression" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected } - - It 'items function handles nested values: ' -TestCases @( - @{ expression = "[items(createObject('person', createObject('name', 'John')))[0].value.name]"; expected = 'John' } - @{ expression = "[items(createObject('list', createArray('a','b','c')))[0].value[1]]"; expected = 'b' } - @{ expression = "[length(items(createObject('obj', createObject('x', 1, 'y', 2)))[0].value)]"; expected = 2 } - ) { - param($expression, $expected) - - $escapedExpression = $expression -replace "'", "''" + + It 'uriComponentToString function works with round-trip encoding' { + $original = 'hello world' + $expected = $original + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: '$escapedExpression' + output: "[uriComponentToString(uriComponent('hello world'))]" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected } - - It 'uriComponent function works for: ' -TestCases @( - @{ expression = "[uriComponent('hello world')]"; expected = 'hello%20world' } - @{ expression = "[uriComponent('hello@example.com')]"; expected = 'hello%40example.com' } - @{ expression = "[uriComponent('https://example.com/path?query=value')]"; expected = 'https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue' } - @{ expression = "[uriComponent('')]"; expected = '' } - @{ expression = "[uriComponent('ABCabc123-_.~')]"; expected = 'ABCabc123-_.~' } - @{ expression = "[uriComponent(':/?#[]@!$&()*+,;=')]"; expected = '%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D' } - @{ expression = "[uriComponent('café')]"; expected = 'caf%C3%A9' } - @{ expression = "[uriComponent('name=John Doe&age=30')]"; expected = 'name%3DJohn%20Doe%26age%3D30' } - @{ expression = "[uriComponent('/path/to/my file.txt')]"; expected = '%2Fpath%2Fto%2Fmy%20file.txt' } - @{ expression = "[uriComponent(concat('hello', ' ', 'world'))]"; expected = 'hello%20world' } - @{ expression = "[uriComponent('user+tag@example.com')]"; expected = 'user%2Btag%40example.com' } - @{ expression = "[uriComponent('1234567890')]"; expected = '1234567890' } - @{ expression = "[uriComponent('100%')]"; expected = '100%25' } - @{ expression = "[uriComponent(' ')]"; expected = '%20' } - ) { - param($expression, $expected) + + It 'uriComponentToString function works with nested round-trip' { + $original = 'user+tag@example.com' + $expected = $original + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: "$expression" + output: "[uriComponentToString(uriComponent('user+tag@example.com'))]" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected } - - It 'uriComponentToString function works for: ' -TestCases @( - @{ expression = "[uriComponentToString('hello%20world')]"; expected = 'hello world' } - @{ expression = "[uriComponentToString('hello%40example.com')]"; expected = 'hello@example.com' } - @{ expression = "[uriComponentToString('https%3A%2F%2Fexample.com%2Fpath%3Fquery%3Dvalue')]"; expected = 'https://example.com/path?query=value' } - @{ expression = "[uriComponentToString('')]"; expected = '' } - @{ expression = "[uriComponentToString('ABCabc123-_.~')]"; expected = 'ABCabc123-_.~' } - @{ expression = "[uriComponentToString('%3A%2F%3F%23%5B%5D%40%21%24%26%28%29%2A%2B%2C%3B%3D')]"; expected = ':/?#[]@!$&()*+,;=' } - @{ expression = "[uriComponentToString('caf%C3%A9')]"; expected = 'café' } - @{ expression = "[uriComponentToString('name%3DJohn%20Doe%26age%3D30')]"; expected = 'name=John Doe&age=30' } - @{ expression = "[uriComponentToString('%2Fpath%2Fto%2Fmy%20file.txt')]"; expected = '/path/to/my file.txt' } - @{ expression = "[uriComponentToString(uriComponent('hello world'))]"; expected = 'hello world' } - @{ expression = "[uriComponentToString(uriComponent('user+tag@example.com'))]"; expected = 'user+tag@example.com' } - @{ expression = "[uriComponentToString('100%25')]"; expected = '100%' } - @{ expression = "[uriComponentToString(concat('hello', '%20', 'world'))]"; expected = 'hello world' } - ) { - param($expression, $expected) + + It 'uriComponentToString function works with concat' { + $input1 = 'hello' + $input2 = '%20' + $input3 = 'world' + $expected = [Uri]::UnescapeDataString($input1 + $input2 + $input3) + $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: Echo type: Microsoft.DSC.Debug/Echo properties: - output: "$expression" + output: "[uriComponentToString(concat('hello', '%20', 'world'))]" "@ $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - $out.results[0].result.actualState.output | Should -Be $expected + $out.results[0].result.actualState.output | Should -BeExactly $expected } } \ No newline at end of file From bfa01154c9823a4e257b62704f1df51855b9f1ed Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Thu, 16 Oct 2025 02:41:39 +0200 Subject: [PATCH 18/19] Uncomment test --- dsc/tests/dsc_functions.tests.ps1 | 1680 ++++++++++++++--------------- 1 file changed, 840 insertions(+), 840 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 995306abf..27440512a 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -2,906 +2,906 @@ # Licensed under the MIT License. Describe 'tests for function expressions' { -# It 'function works: ' -TestCases @( -# @{ text = "[concat('a', 'b')]"; expected = 'ab' } -# @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } -# @{ text = "[concat('a', concat('b', 'c'))]"; expected = 'abc' } -# @{ text = "[base64('ab')]"; expected = 'YWI=' } -# @{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' } -# @{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' } -# ) { -# param($text, $expected) + It 'function works: ' -TestCases @( + @{ text = "[concat('a', 'b')]"; expected = 'ab' } + @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } + @{ text = "[concat('a', concat('b', 'c'))]"; expected = 'abc' } + @{ text = "[base64('ab')]"; expected = 'YWI=' } + @{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' } + @{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' } + ) { + param($text, $expected) -# $escapedText = $text -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedText' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedText = $text -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedText' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'path() works' -TestCases @( -# @{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" } -# @{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" } -# ) { -# param($path, $expected) + It 'path() works' -TestCases @( + @{ path = "systemRoot(), 'a'"; expected = "$PSHOME$([System.IO.Path]::DirectorySeparatorChar)a" } + @{ path = "'a', 'b', 'c'"; expected = "a$([System.IO.Path]::DirectorySeparatorChar)b$([System.IO.Path]::DirectorySeparatorChar)c" } + ) { + param($path, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "[path($path)]" -# "@ -# $out = $config_yaml | dsc config --system-root $PSHOME get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -BeExactly $expected -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[path($path)]" +"@ + $out = $config_yaml | dsc config --system-root $PSHOME get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -BeExactly $expected + } -# It 'default systemRoot() is correct for the OS' { -# $config_yaml = @' -# $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "[systemRoot()]" -# '@ + It 'default systemRoot() is correct for the OS' { + $config_yaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[systemRoot()]" +'@ -# $expected = if ($IsWindows) { -# $env:SYSTEMDRIVE + '\' -# } else { -# '/' -# } -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -# $out.results[0].result.actualState.output | Should -BeExactly $expected -# } + $expected = if ($IsWindows) { + $env:SYSTEMDRIVE + '\' + } else { + '/' + } + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly $expected + } -# It 'union function works for: ' -TestCases @( -# @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } -# @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } -# @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } -# @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } -# @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } -# ) { -# param($expression, $expected, $isError) + It 'union function works for: ' -TestCases @( + @{ expression = "[union(parameters('firstArray'), parameters('secondArray'))]"; expected = @('ab', 'cd', 'ef') } + @{ expression = "[union(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ one = 'a'; two = 'c'; three = 'd' } } + @{ expression = "[union(parameters('secondArray'), parameters('secondArray'))]"; expected = @('cd', 'ef') } + @{ expression = "[union(parameters('secondObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'c'; three = 'd' } } + @{ expression = "[union(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } + ) { + param($expression, $expected, $isError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# parameters: -# firstObject: -# type: object -# defaultValue: -# one: a -# two: b -# secondObject: -# type: object -# defaultValue: -# two: c -# three: d -# firstArray: -# type: array -# defaultValue: -# - ab -# - cd -# secondArray: -# type: array -# defaultValue: -# - cd -# - ef -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# if ($isError) { -# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) -# (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' -# } else { -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + firstObject: + type: object + defaultValue: + one: a + two: b + secondObject: + type: object + defaultValue: + two: c + three: d + firstArray: + type: array + defaultValue: + - ab + - cd + secondArray: + type: array + defaultValue: + - cd + - ef + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + } -# It 'intersection function works for: ' -TestCases @( -# @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') } -# @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } } -# @{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') } -# @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } -# @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } -# @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } -# @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } -# @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } -# @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ -# shared = [pscustomobject]@{ value = 42; flag = $true } -# level = 1 -# } -# } -# @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } -# @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } -# ) { -# param($expression, $expected, $isError) + It 'intersection function works for: ' -TestCases @( + @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'))]"; expected = @('cd') } + @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'))]"; expected = [pscustomobject]@{ two = 'b' } } + @{ expression = "[intersection(parameters('thirdArray'), parameters('fourthArray'))]"; expected = @('ef', 'gh') } + @{ expression = "[intersection(parameters('thirdObject'), parameters('fourthObject'))]"; expected = [pscustomobject]@{ three = 'd' } } + @{ expression = "[intersection(parameters('firstArray'), parameters('thirdArray'))]"; expected = @() } + @{ expression = "[intersection(parameters('firstObject'), parameters('firstArray'))]"; isError = $true } + @{ expression = "[intersection(parameters('firstArray'), parameters('secondArray'), parameters('fifthArray'))]"; expected = @('cd') } + @{ expression = "[intersection(parameters('firstObject'), parameters('secondObject'), parameters('sixthObject'))]"; expected = [pscustomobject]@{ two = 'b' } } + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{ + shared = [pscustomobject]@{ value = 42; flag = $true } + level = 1 + } + } + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } } + @{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } } + ) { + param($expression, $expected, $isError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# parameters: -# firstObject: -# type: object -# defaultValue: -# one: a -# two: b -# secondObject: -# type: object -# defaultValue: -# two: b -# three: d -# thirdObject: -# type: object -# defaultValue: -# two: c -# three: d -# fourthObject: -# type: object -# defaultValue: -# three: d -# four: e -# sixthObject: -# type: object -# defaultValue: -# two: b -# five: f -# nestedObject1: -# type: object -# defaultValue: -# shared: -# value: 42 -# flag: true -# level: 1 -# unique1: test -# nestedObject2: -# type: object -# defaultValue: -# shared: -# value: 42 -# flag: true -# level: 1 -# unique2: test -# nestedObject3: -# type: object -# defaultValue: -# shared: -# value: 24 -# flag: true -# level: 1 -# unique3: test -# nestedObject4: -# type: object -# defaultValue: -# level: 1 -# different: -# value: 100 -# flag: false -# firstArray: -# type: array -# defaultValue: -# - ab -# - cd -# secondArray: -# type: array -# defaultValue: -# - cd -# - ef -# thirdArray: -# type: array -# defaultValue: -# - ef -# - gh -# fourthArray: -# type: array -# defaultValue: -# - gh -# - ef -# - ij -# fifthArray: -# type: array -# defaultValue: -# - cd -# - kl -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# if ($isError) { -# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) -# (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' -# } else { -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + firstObject: + type: object + defaultValue: + one: a + two: b + secondObject: + type: object + defaultValue: + two: b + three: d + thirdObject: + type: object + defaultValue: + two: c + three: d + fourthObject: + type: object + defaultValue: + three: d + four: e + sixthObject: + type: object + defaultValue: + two: b + five: f + nestedObject1: + type: object + defaultValue: + shared: + value: 42 + flag: true + level: 1 + unique1: test + nestedObject2: + type: object + defaultValue: + shared: + value: 42 + flag: true + level: 1 + unique2: test + nestedObject3: + type: object + defaultValue: + shared: + value: 24 + flag: true + level: 1 + unique3: test + nestedObject4: + type: object + defaultValue: + level: 1 + different: + value: 100 + flag: false + firstArray: + type: array + defaultValue: + - ab + - cd + secondArray: + type: array + defaultValue: + - cd + - ef + thirdArray: + type: array + defaultValue: + - ef + - gh + fourthArray: + type: array + defaultValue: + - gh + - ef + - ij + fifthArray: + type: array + defaultValue: + - cd + - kl + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'All arguments must either be arrays or objects' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + } -# It 'contain function works for: ' -TestCases @( -# @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } -# @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } -# @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } -# @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } -# @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } -# @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } -# @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } -# @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } -# @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } -# @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } -# @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } -# @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } -# ) { -# param($expression, $expected, $isError) + It 'contain function works for: ' -TestCases @( + @{ expression = "[contains(parameters('array'), 'a')]" ; expected = $true } + @{ expression = "[contains(parameters('array'), 2)]" ; expected = $false } + @{ expression = "[contains(parameters('array'), 1)]" ; expected = $true } + @{ expression = "[contains(parameters('array'), 'z')]" ; expected = $false } + @{ expression = "[contains(parameters('object'), 'a')]" ; expected = $true } + @{ expression = "[contains(parameters('object'), 'c')]" ; expected = $false } + @{ expression = "[contains(parameters('object'), 3)]" ; expected = $true } + @{ expression = "[contains(parameters('object'), parameters('object'))]" ; isError = $true } + @{ expression = "[contains(parameters('array'), parameters('array'))]" ; isError = $true } + @{ expression = "[contains(parameters('string'), 'not found')]" ; expected = $false } + @{ expression = "[contains(parameters('string'), 'hello')]" ; expected = $true } + @{ expression = "[contains(parameters('string'), 12)]" ; expected = $true } + ) { + param($expression, $expected, $isError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# parameters: -# array: -# type: array -# defaultValue: -# - a -# - b -# - 0 -# - 1 -# object: -# type: object -# defaultValue: -# a: 1 -# b: 2 -# 3: c -# string: -# type: string -# defaultValue: 'hello 123 world!' -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# if ($isError) { -# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) -# (Get-Content $TestDrive/error.log -Raw) | Should -Match 'accepted types are: String, Number' -# } else { -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + array: + type: array + defaultValue: + - a + - b + - 0 + - 1 + object: + type: object + defaultValue: + a: 1 + b: 2 + 3: c + string: + type: string + defaultValue: 'hello 123 world!' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + if ($isError) { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + (Get-Content $TestDrive/error.log -Raw) | Should -Match 'accepted types are: String, Number' + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } + } -# It 'length function works for: ' -TestCases @( -# @{ expression = "[length(parameters('array'))]" ; expected = 3 } -# @{ expression = "[length(parameters('object'))]" ; expected = 4 } -# @{ expression = "[length(parameters('string'))]" ; expected = 12 } -# @{ expression = "[length('')]"; expected = 0 } -# ) { -# param($expression, $expected, $isError) + It 'length function works for: ' -TestCases @( + @{ expression = "[length(parameters('array'))]" ; expected = 3 } + @{ expression = "[length(parameters('object'))]" ; expected = 4 } + @{ expression = "[length(parameters('string'))]" ; expected = 12 } + @{ expression = "[length('')]"; expected = 0 } + ) { + param($expression, $expected, $isError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# parameters: -# array: -# type: array -# defaultValue: -# - a -# - b -# - c -# object: -# type: object -# defaultValue: -# one: a -# two: b -# three: c -# four: d -# string: -# type: string -# defaultValue: 'hello world!' -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + array: + type: array + defaultValue: + - a + - b + - c + object: + type: object + defaultValue: + one: a + two: b + three: c + four: d + string: + type: string + defaultValue: 'hello world!' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'empty function works for: ' -TestCases @( -# @{ expression = "[empty(parameters('array'))]" ; expected = $false } -# @{ expression = "[empty(parameters('object'))]" ; expected = $false } -# @{ expression = "[empty(parameters('string'))]" ; expected = $false } -# @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } -# @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } -# @{ expression = "[empty('')]" ; expected = $true } -# ) { -# param($expression, $expected) + It 'empty function works for: ' -TestCases @( + @{ expression = "[empty(parameters('array'))]" ; expected = $false } + @{ expression = "[empty(parameters('object'))]" ; expected = $false } + @{ expression = "[empty(parameters('string'))]" ; expected = $false } + @{ expression = "[empty(parameters('emptyArray'))]" ; expected = $true } + @{ expression = "[empty(parameters('emptyObject'))]" ; expected = $true } + @{ expression = "[empty('')]" ; expected = $true } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# parameters: -# array: -# type: array -# defaultValue: -# - a -# - b -# - c -# emptyArray: -# type: array -# defaultValue: [] -# object: -# type: object -# defaultValue: -# one: a -# two: b -# three: c -# emptyObject: -# type: object -# defaultValue: {} -# string: -# type: string -# defaultValue: 'hello world!' -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + array: + type: array + defaultValue: + - a + - b + - c + emptyArray: + type: array + defaultValue: [] + object: + type: object + defaultValue: + one: a + two: b + three: c + emptyObject: + type: object + defaultValue: {} + string: + type: string + defaultValue: 'hello world!' + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'utcNow function works for: utcNow()' -TestCases @( -# @{ format = $null; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$' } -# @{ format = "yyyy-MM-dd"; regex = '^\d{4}-\d{2}-\d{2}$' } -# @{ format = "yyyy-MM-ddTHH"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}$' } -# @{ format = "yyyy-MM-ddTHHZ"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}Z$' } -# @{ format = "MMM dd, yyyy HH"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{2}, \d{4} \d{2}$' } -# @{ format = "yy-MMMM-dddd tt H"; regex = '^\d{2}-(January|February|March|April|May|June|July|August|September|October|November|December)-(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday) (AM|PM) \d+$' } -# @{ format = "MMM ddd zzz"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (Sun|Mon|Tue|Wed|Thu|Fri|Sat) \+00:00$' } -# @{ format = "yy yyyy MM MMM MMMM"; regex = '^\d{2} \d{4} \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (January|February|March|April|May|June|July|August|September|October|November|December)$' } -# @{ format = "yyyy-MM-ddTHH:mm:ss"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$' } -# ) { -# param($format, $regex) + It 'utcNow function works for: utcNow()' -TestCases @( + @{ format = $null; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$' } + @{ format = "yyyy-MM-dd"; regex = '^\d{4}-\d{2}-\d{2}$' } + @{ format = "yyyy-MM-ddTHH"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}$' } + @{ format = "yyyy-MM-ddTHHZ"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}Z$' } + @{ format = "MMM dd, yyyy HH"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{2}, \d{4} \d{2}$' } + @{ format = "yy-MMMM-dddd tt H"; regex = '^\d{2}-(January|February|March|April|May|June|July|August|September|October|November|December)-(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday) (AM|PM) \d+$' } + @{ format = "MMM ddd zzz"; regex = '^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (Sun|Mon|Tue|Wed|Thu|Fri|Sat) \+00:00$' } + @{ format = "yy yyyy MM MMM MMMM"; regex = '^\d{2} \d{4} \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (January|February|March|April|May|June|July|August|September|October|November|December)$' } + @{ format = "yyyy-MM-ddTHH:mm:ss"; regex = '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$' } + ) { + param($format, $regex) -# if ($null -ne $format) { -# $format = "'$format'" -# } + if ($null -ne $format) { + $format = "'$format'" + } -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# parameters: -# test: -# type: string -# defaultValue: "[utcNow($format)]" -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "[parameters('test')]" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# # ConvertFrom-Json will convert the date to a DateTime object, so we use regex to capture the string -# $out -match '"output":"(?.*?)"' | Should -BeTrue -Because "Output should contain a date" -# $actual = $matches['date'] -# # compare against the regex -# $actual | Should -Match $regex -Because "Output date '$actual' should match regex '$regex'" -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + test: + type: string + defaultValue: "[utcNow($format)]" + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[parameters('test')]" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + # ConvertFrom-Json will convert the date to a DateTime object, so we use regex to capture the string + $out -match '"output":"(?.*?)"' | Should -BeTrue -Because "Output should contain a date" + $actual = $matches['date'] + # compare against the regex + $actual | Should -Match $regex -Because "Output date '$actual' should match regex '$regex'" + } -# It 'utcNow errors if used not as a parameter default' { -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "[utcNow()]" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) -# $out | Should -BeNullOrEmpty -Because "Output should be null or empty" -# (Get-Content $TestDrive/error.log -Raw) | Should -Match "The 'utcNow\(\)' function can only be used as a parameter default" -# } + It 'utcNow errors if used not as a parameter default' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[utcNow()]" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw) + $out | Should -BeNullOrEmpty -Because "Output should be null or empty" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "The 'utcNow\(\)' function can only be used as a parameter default" + } -# It 'uniqueString function works for: ' -TestCases @( -# @{ expression = "[uniqueString('a')]" ; expected = 'cfvwxu6sc4lqo' } -# @{ expression = "[uniqueString('a', 'b', 'c')]" ; expected = 'bhw7m6t6ntwd6' } -# @{ expression = "[uniqueString('a', 'b', 'c', 'd')]" ; expected = 'yxzg7ur4qetcy' } -# ) { -# param($expression, $expected) + It 'uniqueString function works for: ' -TestCases @( + @{ expression = "[uniqueString('a')]" ; expected = 'cfvwxu6sc4lqo' } + @{ expression = "[uniqueString('a', 'b', 'c')]" ; expected = 'bhw7m6t6ntwd6' } + @{ expression = "[uniqueString('a', 'b', 'c', 'd')]" ; expected = 'yxzg7ur4qetcy' } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# $out.results[0].result.actualState.output | Should -BeExactly $expected -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.actualState.output | Should -BeExactly $expected + } -# It 'string function works for: ' -TestCases @( -# @{ expression = "[string('hello')]"; expected = 'hello' } -# @{ expression = "[string(123)]"; expected = '123' } -# @{ expression = "[string(true)]"; expected = 'true' } -# @{ expression = "[string(null())]"; expected = 'null' } -# @{ expression = "[string(createArray('a', 'b'))]"; expected = '["a","b"]' } -# @{ expression = "[string(createObject('a', 1))]"; expected = '{"a":1}' } -# ) { -# param($expression, $expected) + It 'string function works for: ' -TestCases @( + @{ expression = "[string('hello')]"; expected = 'hello' } + @{ expression = "[string(123)]"; expected = '123' } + @{ expression = "[string(true)]"; expected = 'true' } + @{ expression = "[string(null())]"; expected = 'null' } + @{ expression = "[string(createArray('a', 'b'))]"; expected = '["a","b"]' } + @{ expression = "[string(createObject('a', 1))]"; expected = '{"a":1}' } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'array function works for: ' -TestCases @( -# @{ expression = "[array('hello')]"; expected = @('hello') } -# @{ expression = "[array(42)]"; expected = @(42) } -# @{ expression = "[array(createObject('key', 'value'))]"; expected = @([pscustomobject]@{ key = 'value' }) } -# @{ expression = "[array(createArray('a', 'b'))]"; expected = @(@('a', 'b')) } -# ) { -# param($expression, $expected) + It 'array function works for: ' -TestCases @( + @{ expression = "[array('hello')]"; expected = @('hello') } + @{ expression = "[array(42)]"; expected = @(42) } + @{ expression = "[array(createObject('key', 'value'))]"; expected = @([pscustomobject]@{ key = 'value' }) } + @{ expression = "[array(createArray('a', 'b'))]"; expected = @(@('a', 'b')) } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'first function works for: ' -TestCases @( -# @{ expression = "[first(createArray('hello', 'world'))]"; expected = 'hello' } -# @{ expression = "[first(createArray(1, 2, 3))]"; expected = 1 } -# @{ expression = "[first('hello')]"; expected = 'h' } -# @{ expression = "[first('a')]"; expected = 'a' } -# @{ expression = "[first(array('mixed'))]"; expected = 'mixed' } -# ) { -# param($expression, $expected) + It 'first function works for: ' -TestCases @( + @{ expression = "[first(createArray('hello', 'world'))]"; expected = 'hello' } + @{ expression = "[first(createArray(1, 2, 3))]"; expected = 1 } + @{ expression = "[first('hello')]"; expected = 'h' } + @{ expression = "[first('a')]"; expected = 'a' } + @{ expression = "[first(array('mixed'))]"; expected = 'mixed' } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'indexOf function works for: ' -TestCases @( -# @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 } -# @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 } -# @{ expression = "[indexOf(createArray(10, 20, 30), 20)]"; expected = 1 } -# @{ expression = "[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 0 } -# @{ expression = "[indexOf(createArray('apple', 'banana'), 'orange')]"; expected = -1 } -# @{ expression = "[indexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } -# @{ expression = "[indexOf(createArray(), 'test')]"; expected = -1 } -# @{ expression = "[indexOf(createArray(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]"; expected = 1 } -# ) { -# param($expression, $expected) + It 'indexOf function works for: ' -TestCases @( + @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'banana')]"; expected = 1 } + @{ expression = "[indexOf(createArray('apple', 'banana', 'cherry'), 'cherry')]"; expected = 2 } + @{ expression = "[indexOf(createArray(10, 20, 30), 20)]"; expected = 1 } + @{ expression = "[indexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 0 } + @{ expression = "[indexOf(createArray('apple', 'banana'), 'orange')]"; expected = -1 } + @{ expression = "[indexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } + @{ expression = "[indexOf(createArray(), 'test')]"; expected = -1 } + @{ expression = "[indexOf(createArray(createArray('a', 'b'), createArray('c', 'd')), createArray('c', 'd'))]"; expected = 1 } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'join function works for: ' -TestCases @( -# @{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' } -# @{ expression = "[join(createArray(), '-')]"; expected = '' } -# @{ expression = "[join(createArray(1,2,3), ',')]"; expected = '1,2,3' } -# ) { -# param($expression, $expected) + It 'join function works for: ' -TestCases @( + @{ expression = "[join(createArray('a','b','c'), '-')]"; expected = 'a-b-c' } + @{ expression = "[join(createArray(), '-')]"; expected = '' } + @{ expression = "[join(createArray(1,2,3), ',')]"; expected = '1,2,3' } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'skip function works for: ' -TestCases @( -# @{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') } -# @{ expression = "[skip('hello', 2)]"; expected = 'llo' } -# @{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a', 'b') } -# @{ expression = "[skip('abc', 0)]"; expected = 'abc' } -# @{ expression = "[skip(createArray('a','b'), 5)]"; expected = @() } -# @{ expression = "[skip('', 1)]"; expected = '' } -# # Negative counts are treated as zero -# @{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x', 'y') } -# @{ expression = "[skip('xy', -1)]"; expected = 'xy' } -# ) { -# param($expression, $expected) + It 'skip function works for: ' -TestCases @( + @{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') } + @{ expression = "[skip('hello', 2)]"; expected = 'llo' } + @{ expression = "[skip(createArray('a','b'), 0)]"; expected = @('a', 'b') } + @{ expression = "[skip('abc', 0)]"; expected = 'abc' } + @{ expression = "[skip(createArray('a','b'), 5)]"; expected = @() } + @{ expression = "[skip('', 1)]"; expected = '' } + # Negative counts are treated as zero + @{ expression = "[skip(createArray('x','y'), -3)]"; expected = @('x', 'y') } + @{ expression = "[skip('xy', -1)]"; expected = 'xy' } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'lastIndexOf function works for: ' -TestCases @( -# @{ expression = "[lastIndexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 2 } -# @{ expression = "[lastIndexOf(createArray(10, 20, 30, 20), 20)]"; expected = 3 } -# @{ expression = "[lastIndexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } -# @{ expression = "[lastIndexOf(createArray(createArray('a','b'), createArray('c','d'), createArray('a','b')), createArray('a','b'))]"; expected = 2 } -# @{ expression = "[lastIndexOf(createArray(createObject('name','John'), createObject('name','Jane'), createObject('name','John')), createObject('name','John'))]"; expected = 2 } -# @{ expression = "[lastIndexOf(createArray(), 'test')]"; expected = -1 } -# # Objects are compared by deep equality: same keys and values are equal, regardless of property order. -# # Both createObject('a',1,'b',2) and createObject('b',2,'a',1) are considered equal. -# # Therefore, lastIndexOf returns 1 (the last position where an equal object occurs). -# @{ expression = "[lastIndexOf(createArray(createObject('a',1,'b',2), createObject('b',2,'a',1)), createObject('a',1,'b',2))]"; expected = 1 } -# @{ expression = "[lastIndexOf(createArray('1','2','3'), 1)]"; expected = -1 } -# @{ expression = "[lastIndexOf(createArray(1,2,3), '1')]"; expected = -1 } -# ) { -# param($expression, $expected) + It 'lastIndexOf function works for: ' -TestCases @( + @{ expression = "[lastIndexOf(createArray('a', 'b', 'a', 'c'), 'a')]"; expected = 2 } + @{ expression = "[lastIndexOf(createArray(10, 20, 30, 20), 20)]"; expected = 3 } + @{ expression = "[lastIndexOf(createArray('Apple', 'Banana'), 'apple')]"; expected = -1 } + @{ expression = "[lastIndexOf(createArray(createArray('a','b'), createArray('c','d'), createArray('a','b')), createArray('a','b'))]"; expected = 2 } + @{ expression = "[lastIndexOf(createArray(createObject('name','John'), createObject('name','Jane'), createObject('name','John')), createObject('name','John'))]"; expected = 2 } + @{ expression = "[lastIndexOf(createArray(), 'test')]"; expected = -1 } + # Objects are compared by deep equality: same keys and values are equal, regardless of property order. + # Both createObject('a',1,'b',2) and createObject('b',2,'a',1) are considered equal. + # Therefore, lastIndexOf returns 1 (the last position where an equal object occurs). + @{ expression = "[lastIndexOf(createArray(createObject('a',1,'b',2), createObject('b',2,'a',1)), createObject('a',1,'b',2))]"; expected = 1 } + @{ expression = "[lastIndexOf(createArray('1','2','3'), 1)]"; expected = -1 } + @{ expression = "[lastIndexOf(createArray(1,2,3), '1')]"; expected = -1 } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'context function works' { -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "[context()]" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# $context = $out.results[0].result.actualState.output -# $os = osinfo | ConvertFrom-Json -# $context.os.family | Should -BeExactly $os.family -# $context.os.version | Should -BeExactly $os.version -# $context.os.bitness | Should -BeExactly $os.bitness -# $context.os.architecture | Should -BeExactly $os.architecture -# $context.security | Should -BeExactly $out.metadata.'Microsoft.DSC'.securityContext -# } + It 'context function works' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[context()]" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $context = $out.results[0].result.actualState.output + $os = osinfo | ConvertFrom-Json + $context.os.family | Should -BeExactly $os.family + $context.os.version | Should -BeExactly $os.version + $context.os.bitness | Should -BeExactly $os.bitness + $context.os.architecture | Should -BeExactly $os.architecture + $context.security | Should -BeExactly $out.metadata.'Microsoft.DSC'.securityContext + } -# It 'range function works: ' -TestCases @( -# @{ expression = '[range(1, 3)]'; expected = @(1, 2, 3) } -# @{ expression = '[range(0, 5)]'; expected = @(0, 1, 2, 3, 4) } -# @{ expression = '[range(-2, 4)]'; expected = @(-2, -1, 0, 1) } -# @{ expression = '[range(10, 0)]'; expected = @() } -# @{ expression = '[range(100, 3)]'; expected = @(100, 101, 102) } -# @{ expression = '[first(range(2147473647, 10000))]'; expected = 2147473647 } -# ) { -# param($expression, $expected) + It 'range function works: ' -TestCases @( + @{ expression = '[range(1, 3)]'; expected = @(1, 2, 3) } + @{ expression = '[range(0, 5)]'; expected = @(0, 1, 2, 3, 4) } + @{ expression = '[range(-2, 4)]'; expected = @(-2, -1, 0, 1) } + @{ expression = '[range(10, 0)]'; expected = @() } + @{ expression = '[range(100, 3)]'; expected = @(100, 101, 102) } + @{ expression = '[first(range(2147473647, 10000))]'; expected = 2147473647 } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } -# It 'range function handles errors correctly: ' -TestCases @( -# @{ expression = '[range(1, -1)]'; expectedError = 'Count must be non-negative' } -# @{ expression = '[range(1, 10001)]'; expectedError = 'Count must not exceed 10000' } -# @{ expression = '[range(2147483647, 1)]'; expectedError = 'Sum of startIndex and count must not exceed 2147483647' } -# ) { -# param($expression, $expectedError) + It 'range function handles errors correctly: ' -TestCases @( + @{ expression = '[range(1, -1)]'; expectedError = 'Count must be non-negative' } + @{ expression = '[range(1, 10001)]'; expectedError = 'Count must not exceed 10000' } + @{ expression = '[range(2147483647, 1)]'; expectedError = 'Sum of startIndex and count must not exceed 2147483647' } + ) { + param($expression, $expectedError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log -# $LASTEXITCODE | Should -Not -Be 0 -# $errorContent = Get-Content $TestDrive/error.log -Raw -# $errorContent | Should -Match ([regex]::Escape($expectedError)) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match ([regex]::Escape($expectedError)) + } -# It 'substring function works for: ' -TestCases @( -# @{ expression = "[substring('hello world', 6, 5)]"; expected = 'world' } -# @{ expression = "[substring('hello', 0, 2)]"; expected = 'he' } -# @{ expression = "[substring('hello', 1, 3)]"; expected = 'ell' } -# @{ expression = "[substring('hello', 2)]"; expected = 'llo' } -# @{ expression = "[substring('hello', 0)]"; expected = 'hello' } -# @{ expression = "[substring('hello', 5)]"; expected = '' } -# @{ expression = "[substring('hello', 1, 1)]"; expected = 'e' } -# @{ expression = "[substring('hello', 5, 0)]"; expected = '' } -# @{ expression = "[substring('', 0)]"; expected = '' } -# @{ expression = "[substring('', 0, 0)]"; expected = '' } -# @{ expression = "[substring('héllo', 1, 2)]"; expected = 'él' } -# ) { -# param($expression, $expected) + It 'substring function works for: ' -TestCases @( + @{ expression = "[substring('hello world', 6, 5)]"; expected = 'world' } + @{ expression = "[substring('hello', 0, 2)]"; expected = 'he' } + @{ expression = "[substring('hello', 1, 3)]"; expected = 'ell' } + @{ expression = "[substring('hello', 2)]"; expected = 'llo' } + @{ expression = "[substring('hello', 0)]"; expected = 'hello' } + @{ expression = "[substring('hello', 5)]"; expected = '' } + @{ expression = "[substring('hello', 1, 1)]"; expected = 'e' } + @{ expression = "[substring('hello', 5, 0)]"; expected = '' } + @{ expression = "[substring('', 0)]"; expected = '' } + @{ expression = "[substring('', 0, 0)]"; expected = '' } + @{ expression = "[substring('héllo', 1, 2)]"; expected = 'él' } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'substring function error handling: ' -TestCases @( -# @{ expression = "[substring('hello', -1, 2)]"; expectedError = 'Start index cannot be negative' } -# @{ expression = "[substring('hello', 1, -1)]"; expectedError = 'Length cannot be negative' } -# @{ expression = "[substring('hello', 10, 1)]"; expectedError = 'Start index is beyond the end of the string' } -# @{ expression = "[substring('hello', 2, 10)]"; expectedError = 'Length extends beyond the end of the string' } -# ) { -# param($expression, $expectedError) + It 'substring function error handling: ' -TestCases @( + @{ expression = "[substring('hello', -1, 2)]"; expectedError = 'Start index cannot be negative' } + @{ expression = "[substring('hello', 1, -1)]"; expectedError = 'Length cannot be negative' } + @{ expression = "[substring('hello', 10, 1)]"; expectedError = 'Start index is beyond the end of the string' } + @{ expression = "[substring('hello', 2, 10)]"; expectedError = 'Length extends beyond the end of the string' } + ) { + param($expression, $expectedError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: `"$expression`" -# "@ -# $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log -# $LASTEXITCODE | Should -Not -Be 0 -# $errorContent = Get-Content $TestDrive/error.log -Raw -# $errorContent | Should -Match ([regex]::Escape($expectedError)) -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: `"$expression`" +"@ + $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match ([regex]::Escape($expectedError)) + } -# It 'mixed booleans with functions works' -TestCases @( -# @{ expression = "[and(true(), false, not(false))]"; expected = $false } -# @{ expression = "[or(false, false(), not(false()))]"; expected = $true } -# @{ expression = "[and(true(), true, not(false))]"; expected = $true } -# @{ expression = "[or(false, false(), not(true()))]"; expected = $false } -# ) { -# param($expression, $expected) + It 'mixed booleans with functions works' -TestCases @( + @{ expression = "[and(true(), false, not(false))]"; expected = $false } + @{ expression = "[or(false, false(), not(false()))]"; expected = $true } + @{ expression = "[and(true(), true, not(false))]"; expected = $true } + @{ expression = "[or(false, false(), not(true()))]"; expected = $false } + ) { + param($expression, $expected) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: "$expression" -# "@ -# $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -# $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) -# $out.results[0].result.actualState.output | Should -BeExactly $expected -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "$expression" +"@ + $out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.actualState.output | Should -BeExactly $expected + } -# It 'base64ToString function works for: ' -TestCases @( -# @{ expression = "[base64ToString('aGVsbG8gd29ybGQ=')]"; expected = 'hello world' } -# @{ expression = "[base64ToString('')]"; expected = '' } -# @{ expression = "[base64ToString('aMOpbGxv')]"; expected = 'héllo' } -# @{ expression = "[base64ToString('eyJrZXkiOiJ2YWx1ZSJ9')]"; expected = '{"key":"value"}' } -# @{ expression = "[base64ToString(base64('test message'))]"; expected = 'test message' } -# ) { -# param($expression, $expected) + It 'base64ToString function works for: ' -TestCases @( + @{ expression = "[base64ToString('aGVsbG8gd29ybGQ=')]"; expected = 'hello world' } + @{ expression = "[base64ToString('')]"; expected = '' } + @{ expression = "[base64ToString('aMOpbGxv')]"; expected = 'héllo' } + @{ expression = "[base64ToString('eyJrZXkiOiJ2YWx1ZSJ9')]"; expected = '{"key":"value"}' } + @{ expression = "[base64ToString(base64('test message'))]"; expected = 'test message' } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'base64ToString function error handling: ' -TestCases @( -# @{ expression = "[base64ToString('invalid!@#')]" ; expectedError = 'Invalid base64 encoding' } -# @{ expression = "[base64ToString('/w==')]" ; expectedError = 'Decoded bytes do not form valid UTF-8' } -# ) { -# param($expression, $expectedError) + It 'base64ToString function error handling: ' -TestCases @( + @{ expression = "[base64ToString('invalid!@#')]" ; expectedError = 'Invalid base64 encoding' } + @{ expression = "[base64ToString('/w==')]" ; expectedError = 'Decoded bytes do not form valid UTF-8' } + ) { + param($expression, $expectedError) -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: `"$expression`" -# "@ -# $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log -# $LASTEXITCODE | Should -Not -Be 0 -# $errorContent = Get-Content $TestDrive/error.log -Raw -# $errorContent | Should -Match $expectedError -# } + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: `"$expression`" +"@ + $null = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Not -Be 0 + $errorContent = Get-Content $TestDrive/error.log -Raw + $errorContent | Should -Match $expectedError + } -# It 'toUpper function works for: ' -TestCases @( -# @{ expression = "[toUpper('hello world')]"; expected = 'HELLO WORLD' } -# @{ expression = "[toUpper('Hello World')]"; expected = 'HELLO WORLD' } -# @{ expression = "[toUpper('HELLO WORLD')]"; expected = 'HELLO WORLD' } -# @{ expression = "[toUpper('')]"; expected = '' } -# @{ expression = "[toUpper('Hello123!@#')]"; expected = 'HELLO123!@#' } -# @{ expression = "[toUpper('café')]"; expected = 'CAFÉ' } -# @{ expression = "[toUpper(' hello world ')]"; expected = ' HELLO WORLD ' } -# @{ expression = "[toUpper('a')]"; expected = 'A' } -# @{ expression = "[toUpper(concat('hello', ' world'))]"; expected = 'HELLO WORLD' } -# ) { -# param($expression, $expected) + It 'toUpper function works for: ' -TestCases @( + @{ expression = "[toUpper('hello world')]"; expected = 'HELLO WORLD' } + @{ expression = "[toUpper('Hello World')]"; expected = 'HELLO WORLD' } + @{ expression = "[toUpper('HELLO WORLD')]"; expected = 'HELLO WORLD' } + @{ expression = "[toUpper('')]"; expected = '' } + @{ expression = "[toUpper('Hello123!@#')]"; expected = 'HELLO123!@#' } + @{ expression = "[toUpper('café')]"; expected = 'CAFÉ' } + @{ expression = "[toUpper(' hello world ')]"; expected = ' HELLO WORLD ' } + @{ expression = "[toUpper('a')]"; expected = 'A' } + @{ expression = "[toUpper(concat('hello', ' world'))]"; expected = 'HELLO WORLD' } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'toLower function works for: ' -TestCases @( -# @{ expression = "[toLower('HELLO WORLD')]"; expected = 'hello world' } -# @{ expression = "[toLower('Hello World')]"; expected = 'hello world' } -# @{ expression = "[toLower('hello world')]"; expected = 'hello world' } -# @{ expression = "[toLower('')]"; expected = '' } -# @{ expression = "[toLower('HELLO123!@#')]"; expected = 'hello123!@#' } -# @{ expression = "[toLower('CAFÉ')]"; expected = 'café' } -# @{ expression = "[toLower(' HELLO WORLD ')]"; expected = ' hello world ' } -# @{ expression = "[toLower('A')]"; expected = 'a' } -# @{ expression = "[toLower(concat('HELLO', ' WORLD'))]"; expected = 'hello world' } -# ) { -# param($expression, $expected) + It 'toLower function works for: ' -TestCases @( + @{ expression = "[toLower('HELLO WORLD')]"; expected = 'hello world' } + @{ expression = "[toLower('Hello World')]"; expected = 'hello world' } + @{ expression = "[toLower('hello world')]"; expected = 'hello world' } + @{ expression = "[toLower('')]"; expected = '' } + @{ expression = "[toLower('HELLO123!@#')]"; expected = 'hello123!@#' } + @{ expression = "[toLower('CAFÉ')]"; expected = 'café' } + @{ expression = "[toLower(' HELLO WORLD ')]"; expected = ' hello world ' } + @{ expression = "[toLower('A')]"; expected = 'a' } + @{ expression = "[toLower(concat('HELLO', ' WORLD'))]"; expected = 'hello world' } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'trim function works for: ' -TestCases @( -# @{ expression = "[trim(' hello')]"; expected = 'hello' } -# @{ expression = "[trim('hello ')]"; expected = 'hello' } -# @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } -# @{ expression = "[trim('hello')]"; expected = 'hello' } -# @{ expression = "[trim('')]"; expected = '' } -# @{ expression = "[trim(' ')]"; expected = '' } -# @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } -# @{ expression = "[trim(' café ')]"; expected = 'café' } -# @{ expression = "[trim(' a ')]"; expected = 'a' } -# @{ expression = "[trim(concat(' hello', ' '))]"; expected = 'hello' } -# ) { -# param($expression, $expected) + It 'trim function works for: ' -TestCases @( + @{ expression = "[trim(' hello')]"; expected = 'hello' } + @{ expression = "[trim('hello ')]"; expected = 'hello' } + @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } + @{ expression = "[trim('hello')]"; expected = 'hello' } + @{ expression = "[trim('')]"; expected = '' } + @{ expression = "[trim(' ')]"; expected = '' } + @{ expression = "[trim(' hello world ')]"; expected = 'hello world' } + @{ expression = "[trim(' café ')]"; expected = 'café' } + @{ expression = "[trim(' a ')]"; expected = 'a' } + @{ expression = "[trim(concat(' hello', ' '))]"; expected = 'hello' } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'items function converts object to array: ' -TestCases @( -# @{ expression = "[length(items(createObject('a', 1, 'b', 2)))]"; expected = 2 } -# @{ expression = "[length(items(createObject()))]"; expected = 0 } -# @{ expression = "[items(createObject('name', 'John'))[0].key]"; expected = 'name' } -# @{ expression = "[items(createObject('name', 'John'))[0].value]"; expected = 'John' } -# @{ expression = "[items(createObject('a', 1, 'b', 2, 'c', 3))[1].key]"; expected = 'b' } -# @{ expression = "[items(createObject('x', 'hello', 'y', 'world'))[0].value]"; expected = 'hello' } -# ) { -# param($expression, $expected) + It 'items function converts object to array: ' -TestCases @( + @{ expression = "[length(items(createObject('a', 1, 'b', 2)))]"; expected = 2 } + @{ expression = "[length(items(createObject()))]"; expected = 0 } + @{ expression = "[items(createObject('name', 'John'))[0].key]"; expected = 'name' } + @{ expression = "[items(createObject('name', 'John'))[0].value]"; expected = 'John' } + @{ expression = "[items(createObject('a', 1, 'b', 2, 'c', 3))[1].key]"; expected = 'b' } + @{ expression = "[items(createObject('x', 'hello', 'y', 'world'))[0].value]"; expected = 'hello' } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } -# It 'items function handles nested values: ' -TestCases @( -# @{ expression = "[items(createObject('person', createObject('name', 'John')))[0].value.name]"; expected = 'John' } -# @{ expression = "[items(createObject('list', createArray('a','b','c')))[0].value[1]]"; expected = 'b' } -# @{ expression = "[length(items(createObject('obj', createObject('x', 1, 'y', 2)))[0].value)]"; expected = 2 } -# ) { -# param($expression, $expected) + It 'items function handles nested values: ' -TestCases @( + @{ expression = "[items(createObject('person', createObject('name', 'John')))[0].value.name]"; expected = 'John' } + @{ expression = "[items(createObject('list', createArray('a','b','c')))[0].value[1]]"; expected = 'b' } + @{ expression = "[length(items(createObject('obj', createObject('x', 1, 'y', 2)))[0].value)]"; expected = 2 } + ) { + param($expression, $expected) -# $escapedExpression = $expression -replace "'", "''" -# $config_yaml = @" -# `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -# resources: -# - name: Echo -# type: Microsoft.DSC.Debug/Echo -# properties: -# output: '$escapedExpression' -# "@ -# $out = $config_yaml | dsc config get -f - | ConvertFrom-Json -# $out.results[0].result.actualState.output | Should -Be $expected -# } + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + $out.results[0].result.actualState.output | Should -Be $expected + } It 'uriComponent function works for: ' -TestCases @( @{ testInput = 'hello world' } From a80ecfd5760c8de289df8cebe86b199b24d7ca47 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Thu, 16 Oct 2025 02:46:26 +0200 Subject: [PATCH 19/19] Move test --- dsc/tests/dsc_functions.tests.ps1 | 58 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index cc48856ca..5149c40b5 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -903,6 +903,35 @@ Describe 'tests for function expressions' { $out.results[0].result.actualState.output | Should -Be $expected } + It 'tryGet() function works for: ' -TestCases @( + @{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'a')]"; expected = 1 } + @{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'c')]"; expected = $null } + @{ expression = "[tryGet(createObject('key', 'value'), 'key')]"; expected = 'value' } + @{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'nested')]"; expected = [pscustomobject]@{ x = 10 } } + @{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'missing')]"; expected = $null } + @{ expression = "[tryGet(createArray(1,2,3), 0)]"; expected = 1 } + @{ expression = "[tryGet(createArray(1,2,3), 3)]"; expected = $null } + @{ expression = "[tryGet(createArray(1,2,3), -3)]"; expected = $null } + ) { + param($expression, $expected) + + $escapedExpression = $expression -replace "'", "''" + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: '$escapedExpression' +"@ + $out = $config_yaml | dsc config get -f - | ConvertFrom-Json + if ($expected -is [pscustomobject]) { + ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) + } else { + $out.results[0].result.actualState.output | Should -BeExactly $expected + } + } + It 'uriComponent function works for: ' -TestCases @( @{ testInput = 'hello world' } @{ testInput = 'hello@example.com' } @@ -1031,33 +1060,4 @@ Describe 'tests for function expressions' { $out = $config_yaml | dsc config get -f - | ConvertFrom-Json $out.results[0].result.actualState.output | Should -BeExactly $expected } - - It 'tryGet() function works for: ' -TestCases @( - @{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'a')]"; expected = 1 } - @{ expression = "[tryGet(createObject('a', 1, 'b', 2), 'c')]"; expected = $null } - @{ expression = "[tryGet(createObject('key', 'value'), 'key')]"; expected = 'value' } - @{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'nested')]"; expected = [pscustomobject]@{ x = 10 } } - @{ expression = "[tryGet(createObject('nested', createObject('x', 10)), 'missing')]"; expected = $null } - @{ expression = "[tryGet(createArray(1,2,3), 0)]"; expected = 1 } - @{ expression = "[tryGet(createArray(1,2,3), 3)]"; expected = $null } - @{ expression = "[tryGet(createArray(1,2,3), -3)]"; expected = $null } - ) { - param($expression, $expected) - - $escapedExpression = $expression -replace "'", "''" - $config_yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Echo - type: Microsoft.DSC.Debug/Echo - properties: - output: '$escapedExpression' -"@ - $out = $config_yaml | dsc config get -f - | ConvertFrom-Json - if ($expected -is [pscustomobject]) { - ($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String) - } else { - $out.results[0].result.actualState.output | Should -BeExactly $expected - } - } }