From 0b5362241f804f094e179b6b067d3d0d9c603b1f Mon Sep 17 00:00:00 2001 From: Eduard Arbona <175124143+earbona23@users.noreply.github.com> Date: Thu, 3 Sep 2026 08:27:01 -0400 Subject: [PATCH] Fix Windows PowerShell 5.1 import failure from non-ASCII module source (#1603) The per-cmdlet source files under the Applications and Beta.Applications modules contained non-ASCII characters (the arrow "->", em/en dashes, and check marks) inside comments and Write-Host/Write-Verbose strings. These files are concatenated without a BOM into the shipped .psm1. Windows PowerShell 5.1 decodes a BOM-less file using the system ANSI code page instead of UTF-8, so each multi-byte character turns into stray quote characters that unbalance the surrounding strings. The parser then fails with "Missing closing ')' in expression" and "The string is missing the terminator", and Import-Module aborts. PowerShell 7 (Core) reads BOM-less files as UTF-8, which is why CI and PowerShell 7 users never saw the break. Replace the non-ASCII characters with ASCII equivalents so the generated module parses identically under every encoding. Two Users cmdlet files carried the same non-ASCII-without-BOM hazard (a curly apostrophe in a HelpMessage) and are cleaned as well. Add a Pester regression test that fails if any module source file reintroduces non-ASCII bytes. Fixes #1603 --- ...blePermissionsToAgentIdentityBlueprint.ps1 | 6 +-- .../Get-EntraAgentIdentityBlueprint.ps1 | 2 +- ...t-EntraAgentIdentityBlueprintPrincipal.ps1 | 2 +- .../Invoke-EntraAgentIdInteractive.ps1 | 18 ++++---- ...-EntraAgentIDForAgentIdentityBlueprint.ps1 | 2 +- .../New-EntraAgentIdentityBlueprint.ps1 | 2 +- .../Microsoft.Entra/Users/New-EntraUser.ps1 | 2 +- ...blePermissionsToAgentIdentityBlueprint.ps1 | 6 +-- .../Get-EntraBetaAgentIdentityBlueprint.ps1 | 2 +- ...traBetaAgentIdentityBlueprintPrincipal.ps1 | 2 +- .../Grant-EntraBetaMCPServerPermission.ps1 | 2 +- .../Invoke-EntraBetaAgentIdInteractive.ps1 | 18 ++++---- ...raBetaAgentIDForAgentIdentityBlueprint.ps1 | 2 +- .../New-EntraBetaAgentIdentityBlueprint.ps1 | 2 +- .../Revoke-EntraBetaMCPServerPermission.ps1 | 4 +- .../Users/New-EntraBetaUser.ps1 | 2 +- test/Entra/SourceFileEncoding.Tests.ps1 | 44 +++++++++++++++++++ 17 files changed, 81 insertions(+), 37 deletions(-) create mode 100644 test/Entra/SourceFileEncoding.Tests.ps1 diff --git a/module/Entra/Microsoft.Entra/Applications/Add-EntraInheritablePermissionsToAgentIdentityBlueprint.ps1 b/module/Entra/Microsoft.Entra/Applications/Add-EntraInheritablePermissionsToAgentIdentityBlueprint.ps1 index e1ccbb5aee..7b2b3b667a 100644 --- a/module/Entra/Microsoft.Entra/Applications/Add-EntraInheritablePermissionsToAgentIdentityBlueprint.ps1 +++ b/module/Entra/Microsoft.Entra/Applications/Add-EntraInheritablePermissionsToAgentIdentityBlueprint.ps1 @@ -146,7 +146,7 @@ function Add-EntraInheritablePermissionsToAgentIdentityBlueprint { if ($existingEntry) { # Overwrite the existing entry for this resourceAppId - Write-Verbose "Existing inheritable permissions found for resource '$resourceName' — overwriting..." + Write-Verbose "Existing inheritable permissions found for resource '$resourceName' - overwriting..." $patchUrl = "$apiUrl/$($currentResourceAppId.ToString())" Write-Debug "PATCH URL: $patchUrl" @@ -169,8 +169,8 @@ function Add-EntraInheritablePermissionsToAgentIdentityBlueprint { } } else { - # No existing entry for this resourceAppId — add it (preserves other resources' permissions) - Write-Verbose "No existing inheritable permissions for resource '$resourceName' — adding..." + # No existing entry for this resourceAppId - add it (preserves other resources' permissions) + Write-Verbose "No existing inheritable permissions for resource '$resourceName' - adding..." Write-Debug "POST URL: $apiUrl" while ($retryCount -lt $maxRetries -and -not $success) { diff --git a/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprint.ps1 b/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprint.ps1 index 0a74f40cbc..bcd8eb03a6 100644 --- a/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprint.ps1 +++ b/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprint.ps1 @@ -19,7 +19,7 @@ function Get-EntraAgentIdentityBlueprint { return } - # Resolve BlueprintId: explicit param → stored → prompt → error + # Resolve BlueprintId: explicit param -> stored -> prompt -> error if (-not $BlueprintId) { if ((Test-Path variable:script:CurrentAgentBlueprintId) -and $script:CurrentAgentBlueprintId) { $BlueprintId = $script:CurrentAgentBlueprintId diff --git a/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprintPrincipal.ps1 b/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprintPrincipal.ps1 index aa70417b12..0e5456d70e 100644 --- a/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprintPrincipal.ps1 +++ b/module/Entra/Microsoft.Entra/Applications/Get-EntraAgentIdentityBlueprintPrincipal.ps1 @@ -19,7 +19,7 @@ function Get-EntraAgentIdentityBlueprintPrincipal { return } - # Resolve ServicePrincipalId: explicit param → stored → prompt → error + # Resolve ServicePrincipalId: explicit param -> stored -> prompt -> error if (-not $ServicePrincipalId) { if ((Test-Path variable:script:CurrentAgentBlueprintServicePrincipalId) -and $script:CurrentAgentBlueprintServicePrincipalId) { $ServicePrincipalId = $script:CurrentAgentBlueprintServicePrincipalId diff --git a/module/Entra/Microsoft.Entra/Applications/Invoke-EntraAgentIdInteractive.ps1 b/module/Entra/Microsoft.Entra/Applications/Invoke-EntraAgentIdInteractive.ps1 index 096d030e5e..3c3b87ec71 100644 --- a/module/Entra/Microsoft.Entra/Applications/Invoke-EntraAgentIdInteractive.ps1 +++ b/module/Entra/Microsoft.Entra/Applications/Invoke-EntraAgentIdInteractive.ps1 @@ -609,44 +609,44 @@ function Invoke-EntraAgentIdInteractive { # =================================================================== Write-Host "=== Complete Workflow Summary ===" -ForegroundColor Green - Write-Host "✓ 1. Agent Identity Blueprint created and configured" -ForegroundColor Green - Write-Host "✓ 2. Client secret added for API authentication" -ForegroundColor Green + Write-Host "[OK] 1. Agent Identity Blueprint created and configured" -ForegroundColor Green + Write-Host "[OK] 2. Client secret added for API authentication" -ForegroundColor Green if ($hasInteractiveAgents) { - Write-Host "✓ 3. Interactive agent scopes configured with user prompts" -ForegroundColor Green + Write-Host "[OK] 3. Interactive agent scopes configured with user prompts" -ForegroundColor Green } else { Write-Host "- 3. Interactive agent scopes (skipped by user choice)" -ForegroundColor Gray } if ($blueprintWillCreateAgentUsers) { - Write-Host "✓ 4. Blueprint configured to create Agent ID users" -ForegroundColor Green + Write-Host "[OK] 4. Blueprint configured to create Agent ID users" -ForegroundColor Green } else { Write-Host "- 4. Blueprint configured to create Agent ID users (skipped by user choice)" -ForegroundColor Gray } if ($hasInheritablePermissions) { - Write-Host "✓ 5. Inheritable permissions configured for agent users" -ForegroundColor Green + Write-Host "[OK] 5. Inheritable permissions configured for agent users" -ForegroundColor Green } else { Write-Host "- 5. Inheritable permissions (skipped by user choice)" -ForegroundColor Gray } if ($hasInheritablePermissions -and $useStaticPermissions) { - Write-Host "✓ 6. Static permissions configured via required resource access" -ForegroundColor Green + Write-Host "[OK] 6. Static permissions configured via required resource access" -ForegroundColor Green } elseif ($hasInheritablePermissions -and -not $useStaticPermissions) { - Write-Host "✓ 6. Dynamic permissions selected (resolved at runtime)" -ForegroundColor Green + Write-Host "[OK] 6. Dynamic permissions selected (resolved at runtime)" -ForegroundColor Green } else { Write-Host "- 6. Permission model selection (skipped - no inheritable permissions)" -ForegroundColor Gray } - Write-Host "✓ 7. Consent obtained for the blueprint in this tenant" -ForegroundColor Green + Write-Host "[OK] 7. Consent obtained for the blueprint in this tenant" -ForegroundColor Green if ($allAgentIdentities.Count -gt 0) { - Write-Host "✓ 8. Agent Identity and User Creation completed" -ForegroundColor Green + Write-Host "[OK] 8. Agent Identity and User Creation completed" -ForegroundColor Green Write-Host " - Created $($allAgentIdentities.Count) Agent $(if ($allAgentIdentities.Count -eq 1) { 'Identity' } else { 'Identities' })" -ForegroundColor Green if ($blueprintWillCreateAgentUsers) { Write-Host " - Created $($allAgentUsers.Count) Agent $(if ($allAgentUsers.Count -eq 1) { 'User' } else { 'Users' })" -ForegroundColor Green diff --git a/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIDForAgentIdentityBlueprint.ps1 b/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIDForAgentIdentityBlueprint.ps1 index 49d7c6f81b..79ed70188d 100644 --- a/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIDForAgentIdentityBlueprint.ps1 +++ b/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIDForAgentIdentityBlueprint.ps1 @@ -77,7 +77,7 @@ function New-EntraAgentIDForAgentIdentityBlueprint { Write-Verbose "Could not retrieve current user details: $_" } - # Sponsors are always required — prompt until at least one is provided + # Sponsors are always required - prompt until at least one is provided $hasSponsors = (($SponsorUserIds -and $SponsorUserIds.Count -gt 0) -or ($SponsorGroupIds -and $SponsorGroupIds.Count -gt 0)) diff --git a/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIdentityBlueprint.ps1 b/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIdentityBlueprint.ps1 index ec76b7e729..ab43049560 100644 --- a/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIdentityBlueprint.ps1 +++ b/module/Entra/Microsoft.Entra/Applications/New-EntraAgentIdentityBlueprint.ps1 @@ -58,7 +58,7 @@ function New-EntraAgentIdentityBlueprint { $customHeaders = $null } - # Sponsors are always required — prompt until at least one is provided + # Sponsors are always required - prompt until at least one is provided $hasSponsors = (($SponsorUserIds -and $SponsorUserIds.Count -gt 0) -or ($SponsorGroupIds -and $SponsorGroupIds.Count -gt 0)) diff --git a/module/Entra/Microsoft.Entra/Users/New-EntraUser.ps1 b/module/Entra/Microsoft.Entra/Users/New-EntraUser.ps1 index 5152aa3513..05e1bedd7c 100644 --- a/module/Entra/Microsoft.Entra/Users/New-EntraUser.ps1 +++ b/module/Entra/Microsoft.Entra/Users/New-EntraUser.ps1 @@ -83,7 +83,7 @@ function New-EntraUser { [Parameter(ParameterSetName = "CreateUser", HelpMessage = "The user's surname (last name). Maximum length: 64 characters.")] [System.String] $Surname, - [Parameter(ParameterSetName = "CreateUser", HelpMessage = "A list of the user’s additional email addresses (e.g., ['bob@contoso.com', 'Robert@fabrikam.com']). Supports up to 250 entries, each up to 250 characters.")] + [Parameter(ParameterSetName = "CreateUser", HelpMessage = "A list of the user's additional email addresses (e.g., ['bob@contoso.com', 'Robert@fabrikam.com']). Supports up to 250 entries, each up to 250 characters.")] [System.Collections.Generic.List`1[System.String]] $OtherMails, [Parameter(ParameterSetName = "CreateUser", HelpMessage = "The user's sign-in name (UPN) in the format alias@domain. It should match the user's email and use a verified domain in the tenant.")] diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Add-EntraBetaInheritablePermissionsToAgentIdentityBlueprint.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Add-EntraBetaInheritablePermissionsToAgentIdentityBlueprint.ps1 index 68aabdb1e5..25cd520412 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Add-EntraBetaInheritablePermissionsToAgentIdentityBlueprint.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Add-EntraBetaInheritablePermissionsToAgentIdentityBlueprint.ps1 @@ -129,7 +129,7 @@ function Add-EntraBetaInheritablePermissionsToAgentIdentityBlueprint { if ($existingEntry) { # Overwrite the existing entry for this resourceAppId - Write-Verbose "Existing inheritable permissions found for resource '$resourceName' — overwriting..." + Write-Verbose "Existing inheritable permissions found for resource '$resourceName' - overwriting..." $patchUrl = "$apiUrl/$($currentResourceAppId.ToString())" Write-Debug "PATCH URL: $patchUrl" @@ -152,8 +152,8 @@ function Add-EntraBetaInheritablePermissionsToAgentIdentityBlueprint { } } else { - # No existing entry for this resourceAppId — add it (preserves other resources' permissions) - Write-Verbose "No existing inheritable permissions for resource '$resourceName' — adding..." + # No existing entry for this resourceAppId - add it (preserves other resources' permissions) + Write-Verbose "No existing inheritable permissions for resource '$resourceName' - adding..." Write-Debug "POST URL: $apiUrl" while ($retryCount -lt $maxRetries -and -not $success) { diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprint.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprint.ps1 index eb2edc66c3..9a8e827963 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprint.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprint.ps1 @@ -19,7 +19,7 @@ function Get-EntraBetaAgentIdentityBlueprint { return } - # Resolve BlueprintId: explicit param → stored → prompt → error + # Resolve BlueprintId: explicit param -> stored -> prompt -> error if (-not $BlueprintId) { if ((Test-Path variable:script:CurrentAgentBlueprintId) -and $script:CurrentAgentBlueprintId) { $BlueprintId = $script:CurrentAgentBlueprintId diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprintPrincipal.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprintPrincipal.ps1 index 889310db55..d36b21444b 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprintPrincipal.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Get-EntraBetaAgentIdentityBlueprintPrincipal.ps1 @@ -19,7 +19,7 @@ function Get-EntraBetaAgentIdentityBlueprintPrincipal { return } - # Resolve ServicePrincipalId: explicit param → stored → prompt → error + # Resolve ServicePrincipalId: explicit param -> stored -> prompt -> error if (-not $ServicePrincipalId) { if ((Test-Path variable:script:CurrentAgentBlueprintServicePrincipalId) -and $script:CurrentAgentBlueprintServicePrincipalId) { $ServicePrincipalId = $script:CurrentAgentBlueprintServicePrincipalId diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Grant-EntraBetaMCPServerPermission.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Grant-EntraBetaMCPServerPermission.ps1 index 138864aa50..55f023d131 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Grant-EntraBetaMCPServerPermission.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Grant-EntraBetaMCPServerPermission.ps1 @@ -98,7 +98,7 @@ function Grant-EntraBetaMcpServerPermission { $incomingScopes = $targetScopes | Where-Object { $_ } | Sort-Object -Unique if (-not $grant) { - # No existing grant – create with provided scopes (already additive by definition) + # No existing grant - create with provided scopes (already additive by definition) $targetString = ($incomingScopes) -join ' ' Write-Verbose "Creating new permission grant with scopes: $targetString" $body = @{ diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Invoke-EntraBetaAgentIdInteractive.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Invoke-EntraBetaAgentIdInteractive.ps1 index a77c9decba..fc2ca3cb90 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Invoke-EntraBetaAgentIdInteractive.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Invoke-EntraBetaAgentIdInteractive.ps1 @@ -609,44 +609,44 @@ function Invoke-EntraBetaAgentIdInteractive { # =================================================================== Write-Host "=== Complete Workflow Summary ===" -ForegroundColor Green - Write-Host "✓ 1. Agent Identity Blueprint created and configured" -ForegroundColor Green - Write-Host "✓ 2. Client secret added for API authentication" -ForegroundColor Green + Write-Host "[OK] 1. Agent Identity Blueprint created and configured" -ForegroundColor Green + Write-Host "[OK] 2. Client secret added for API authentication" -ForegroundColor Green if ($hasInteractiveAgents) { - Write-Host "✓ 3. Interactive agent scopes configured with user prompts" -ForegroundColor Green + Write-Host "[OK] 3. Interactive agent scopes configured with user prompts" -ForegroundColor Green } else { Write-Host "- 3. Interactive agent scopes (skipped by user choice)" -ForegroundColor Gray } if ($blueprintWillCreateAgentUsers) { - Write-Host "✓ 4. Blueprint configured to create Agent ID users" -ForegroundColor Green + Write-Host "[OK] 4. Blueprint configured to create Agent ID users" -ForegroundColor Green } else { Write-Host "- 4. Blueprint configured to create Agent ID users (skipped by user choice)" -ForegroundColor Gray } if ($hasInheritablePermissions) { - Write-Host "✓ 5. Inheritable permissions configured for agent users" -ForegroundColor Green + Write-Host "[OK] 5. Inheritable permissions configured for agent users" -ForegroundColor Green } else { Write-Host "- 5. Inheritable permissions (skipped by user choice)" -ForegroundColor Gray } if ($hasInheritablePermissions -and $useStaticPermissions) { - Write-Host "✓ 6. Static permissions configured via required resource access" -ForegroundColor Green + Write-Host "[OK] 6. Static permissions configured via required resource access" -ForegroundColor Green } elseif ($hasInheritablePermissions -and -not $useStaticPermissions) { - Write-Host "✓ 6. Dynamic permissions selected (resolved at runtime)" -ForegroundColor Green + Write-Host "[OK] 6. Dynamic permissions selected (resolved at runtime)" -ForegroundColor Green } else { Write-Host "- 6. Permission model selection (skipped - no inheritable permissions)" -ForegroundColor Gray } - Write-Host "✓ 7. Consent obtained for the blueprint in this tenant" -ForegroundColor Green + Write-Host "[OK] 7. Consent obtained for the blueprint in this tenant" -ForegroundColor Green if ($allAgentIdentities.Count -gt 0) { - Write-Host "✓ 8. Agent Identity and User Creation completed" -ForegroundColor Green + Write-Host "[OK] 8. Agent Identity and User Creation completed" -ForegroundColor Green Write-Host " - Created $($allAgentIdentities.Count) Agent $(if ($allAgentIdentities.Count -eq 1) { 'Identity' } else { 'Identities' })" -ForegroundColor Green if ($blueprintWillCreateAgentUsers) { Write-Host " - Created $($allAgentUsers.Count) Agent $(if ($allAgentUsers.Count -eq 1) { 'User' } else { 'Users' })" -ForegroundColor Green diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIDForAgentIdentityBlueprint.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIDForAgentIdentityBlueprint.ps1 index 50eae311dc..1b4534196f 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIDForAgentIdentityBlueprint.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIDForAgentIdentityBlueprint.ps1 @@ -77,7 +77,7 @@ function New-EntraBetaAgentIDForAgentIdentityBlueprint { Write-Verbose "Could not retrieve current user details: $_" } - # Sponsors are always required — prompt until at least one is provided + # Sponsors are always required - prompt until at least one is provided $hasSponsors = (($SponsorUserIds -and $SponsorUserIds.Count -gt 0) -or ($SponsorGroupIds -and $SponsorGroupIds.Count -gt 0)) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIdentityBlueprint.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIdentityBlueprint.ps1 index 63af49452b..29d4636619 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIdentityBlueprint.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/New-EntraBetaAgentIdentityBlueprint.ps1 @@ -58,7 +58,7 @@ function New-EntraBetaAgentIdentityBlueprint { $customHeaders = $null } - # Sponsors are always required — prompt until at least one is provided + # Sponsors are always required - prompt until at least one is provided $hasSponsors = (($SponsorUserIds -and $SponsorUserIds.Count -gt 0) -or ($SponsorGroupIds -and $SponsorGroupIds.Count -gt 0)) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Revoke-EntraBetaMCPServerPermission.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Revoke-EntraBetaMCPServerPermission.ps1 index 8f1a124af8..72c0730efe 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Applications/Revoke-EntraBetaMCPServerPermission.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Applications/Revoke-EntraBetaMCPServerPermission.ps1 @@ -191,10 +191,10 @@ function Revoke-EntraBetaMcpServerPermission { $updatedGrant = Update-GrantScopes -clientSpId $clientSp.Id -resourceSpId $resourceSp.Id -targetScopes $remainingScopes if (@($remainingScopes).Count -eq 0) { - Write-Host "✓ All permissions revoked from $($resolvedClient.Name)" -ForegroundColor Green + Write-Host "[OK] All permissions revoked from $($resolvedClient.Name)" -ForegroundColor Green return $null } else { - Write-Host "✓ Permissions partially revoked from $($resolvedClient.Name)" -ForegroundColor Green + Write-Host "[OK] Permissions partially revoked from $($resolvedClient.Name)" -ForegroundColor Green Write-Verbose " Revoked scopes:" $validScopesToRevoke | ForEach-Object { Write-Verbose " - $_"} Write-Verbose " Remaining scopes:" diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/New-EntraBetaUser.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/New-EntraBetaUser.ps1 index 3f65714508..e0c4e3100b 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/New-EntraBetaUser.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/New-EntraBetaUser.ps1 @@ -83,7 +83,7 @@ function New-EntraBetaUser { [Parameter(ParameterSetName = "CreateUser", HelpMessage = "The user's surname (last name). Maximum length: 64 characters.")] [System.String] $Surname, - [Parameter(ParameterSetName = "CreateUser", HelpMessage = "A list of the user’s additional email addresses (e.g., ['bob@contoso.com', 'Robert@fabrikam.com']). Supports up to 250 entries, each up to 250 characters.")] + [Parameter(ParameterSetName = "CreateUser", HelpMessage = "A list of the user's additional email addresses (e.g., ['bob@contoso.com', 'Robert@fabrikam.com']). Supports up to 250 entries, each up to 250 characters.")] [System.Collections.Generic.List`1[System.String]] $OtherMails, [Parameter(ParameterSetName = "CreateUser", HelpMessage = "The user's sign-in name (UPN) in the format alias@domain. It should match the user's email and use a verified domain in the tenant.")] diff --git a/test/Entra/SourceFileEncoding.Tests.ps1 b/test/Entra/SourceFileEncoding.Tests.ps1 new file mode 100644 index 0000000000..f6da8113e3 --- /dev/null +++ b/test/Entra/SourceFileEncoding.Tests.ps1 @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ + +# Regression guard for issue #1603. +# +# The module .psm1 files are assembled by concatenating the per-cmdlet .ps1 source +# files under module/. When a source file contains non-ASCII characters (for example +# the arrow "->", an em dash, or a check mark) and is saved without a UTF-8 BOM, +# Windows PowerShell 5.1 decodes the bytes using the system ANSI code page instead of +# UTF-8. The mis-decoded multi-byte characters turn into stray quote characters that +# unbalance the surrounding strings, so Import-Module fails with parser errors such as +# "Missing closing ')' in expression" and "The string is missing the terminator". +# +# PowerShell 7 (Core) reads BOM-less files as UTF-8, which is why the CI test runs and +# every day-to-day PowerShell 7 user import the module without error while the module +# is broken on Windows PowerShell 5.1. Keeping the shipped source ASCII-only makes the +# generated .psm1 parse identically under every encoding. + +Describe "Module source file encoding (Windows PowerShell 5.1 import safety)" { + BeforeDiscovery { + $moduleRoot = Join-Path $PSScriptRoot '..' '..' 'module' + $script:SourceFileCases = @() + if (Test-Path -Path $moduleRoot) { + $script:SourceFileCases = Get-ChildItem -Path $moduleRoot -Recurse -Filter '*.ps1' -File | + ForEach-Object { @{ FullName = $_.FullName; Name = $_.Name } } + } + } + + It "Should resolve the module source directory" { + (Join-Path $PSScriptRoot '..' '..' 'module') | Should -Exist + } + + It "Should discover module source files to validate" { + $moduleRoot = Join-Path $PSScriptRoot '..' '..' 'module' + @(Get-ChildItem -Path $moduleRoot -Recurse -Filter '*.ps1' -File).Count | Should -BeGreaterThan 0 + } + + It "'' should not contain non-ASCII characters" -ForEach $script:SourceFileCases { + $bytes = [System.IO.File]::ReadAllBytes($FullName) + $nonAsciiCount = @($bytes | Where-Object { $_ -gt 0x7F }).Count + $nonAsciiCount | Should -Be 0 -Because "$Name must be ASCII-only so Windows PowerShell 5.1 imports the module regardless of a BOM (issue #1603)" + } +}