From fe5085e23be41d13cfae5583d51c56dbead53231 Mon Sep 17 00:00:00 2001 From: Initzero Date: Mon, 5 Oct 2026 07:19:48 +0200 Subject: [PATCH] Restore email and URL validators with explicit syntax rules --- CHANGELOG.md | 10 +++ IT-ToolBox.psd1 | 2 + IT-ToolBox.psm1 | 2 + Private/Test-ITToolBoxDnsName.ps1 | 21 ++++++ Public/Test-IsEmail.ps1 | 48 ++++++++++++ Public/Test-IsUrl.ps1 | 55 ++++++++++++++ README.md | 38 +++++++++- Staging/v3/README.md | 3 +- Staging/v3/Test-IsEmail.ps1 | 49 ------------- Staging/v3/Test-IsUrl.ps1 | 34 --------- Tests/EmailUrlValidation.Tests.ps1 | 113 +++++++++++++++++++++++++++++ Tests/Module.Tests.ps1 | 3 +- 12 files changed, 289 insertions(+), 89 deletions(-) create mode 100644 Private/Test-ITToolBoxDnsName.ps1 create mode 100644 Public/Test-IsEmail.ps1 create mode 100644 Public/Test-IsUrl.ps1 delete mode 100644 Staging/v3/Test-IsEmail.ps1 delete mode 100644 Staging/v3/Test-IsUrl.ps1 create mode 100644 Tests/EmailUrlValidation.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8844044..b24ba62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## Email and URL validation restoration (unreleased) + +- Restore Test-IsEmail and Test-IsUrl; export twenty supported commands. +- Preserve email parameter aliases and the HTTP/HTTPS/FTP/FTPS scheme set. +- Define common bare email syntax with IDN-domain support and explicit length limits. +- Replace the URL regex with structured parsing and validation of hosts, ports and escapes. +- Reject malformed input with false; add per-record pipeline support without network calls. +- Document intentional validation-policy changes and reduce staged candidates to seven. +- Add 96 validator cases covering boundaries, Unicode, IPv6 and malformed inputs. + ## Logging and timer audit fixes (unreleased) - Treat redaction replacement text literally, preventing regex substitutions from reinserting secrets. diff --git a/IT-ToolBox.psd1 b/IT-ToolBox.psd1 index 7580717..4fb03b8 100644 --- a/IT-ToolBox.psd1 +++ b/IT-ToolBox.psd1 @@ -18,6 +18,8 @@ 'Test-IsValidPath' 'Test-IsIP' 'Test-IsDate' + 'Test-IsEmail' + 'Test-IsUrl' 'New-StringEncryption' 'New-StringDecryption' 'New-RandomString' diff --git a/IT-ToolBox.psm1 b/IT-ToolBox.psm1 index 99f6894..8a6fe15 100644 --- a/IT-ToolBox.psm1 +++ b/IT-ToolBox.psm1 @@ -18,6 +18,8 @@ Export-ModuleMember -Function @( 'Test-IsValidPath' 'Test-IsIP' 'Test-IsDate' + 'Test-IsEmail' + 'Test-IsUrl' 'New-StringEncryption' 'New-StringDecryption' 'New-RandomString' diff --git a/Private/Test-ITToolBoxDnsName.ps1 b/Private/Test-ITToolBoxDnsName.ps1 new file mode 100644 index 0000000..55d77ad --- /dev/null +++ b/Private/Test-ITToolBoxDnsName.ps1 @@ -0,0 +1,21 @@ +function Test-ITToolBoxDnsName { + param([string]$Name, [switch]$AllowRootDot) + + if ([string]::IsNullOrWhiteSpace($Name)) { return $false } + if ($AllowRootDot -and $Name.EndsWith('.')) { $Name = $Name.Substring(0, $Name.Length - 1) } + + try { + $idn = [System.Globalization.IdnMapping]::new() + $idn.UseStd3AsciiRules = $true + $ascii = $idn.GetAscii($Name) + } + catch [System.ArgumentException] { return $false } + + if ($ascii.Length -gt 253) { return $false } + foreach ($label in $ascii.Split('.')) { + if ($label.Length -gt 63 -or $label -cnotmatch '^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$') { + return $false + } + } + return $true +} diff --git a/Public/Test-IsEmail.ps1 b/Public/Test-IsEmail.ps1 new file mode 100644 index 0000000..ea4ee0d --- /dev/null +++ b/Public/Test-IsEmail.ps1 @@ -0,0 +1,48 @@ +function Test-IsEmail { + <# + .SYNOPSIS + Tests common bare email-address syntax without contacting a mail server. + .DESCRIPTION + Accepts an ASCII dot-atom local part and a DNS domain, including an IDN domain + and single-label names. The local part is limited to 64 ASCII characters and + the complete address to 254 characters after IDN conversion. + Rejects display names, comments, quoted local parts, address literals, Unicode + local parts, surrounding whitespace and controls. This is a practical subset, + not a complete RFC parser or proof of mailbox existence or deliverability. + .PARAMETER EmailAddress + A bare email address. Null, empty and malformed values return false. + .EXAMPLE + Test-IsEmail -EmailAddress 'person+tag@example.com' + .EXAMPLE + 'person@example.com', 'invalid' | Test-IsEmail + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [AllowNull()] + [AllowEmptyString()] + [Alias('Email', 'Mail', 'Address')] + [string]$EmailAddress + ) + + process { + if ([string]::IsNullOrWhiteSpace($EmailAddress) -or $EmailAddress -match '[\s\p{Cc}]') { + return $false + } + [System.Net.Mail.MailAddress]$parsed = $null + if (-not [System.Net.Mail.MailAddress]::TryCreate($EmailAddress, [ref]$parsed) -or + $parsed.Address -cne $EmailAddress -or $parsed.DisplayName.Length -gt 0) { + return $false + } + + # ASCII dot-atom policy deliberately excludes quoted and internationalized local parts. + if ($parsed.User.Length -gt 64 -or + $parsed.User -cnotmatch '^[a-zA-Z0-9!#$%&''*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&''*+/=?^_`{|}~-]+)*$') { + return $false + } + if (-not (Test-ITToolBoxDnsName -Name $parsed.Host)) { return $false } + $asciiDomain = [System.Globalization.IdnMapping]::new().GetAscii($parsed.Host) + return ($parsed.User.Length + 1 + $asciiDomain.Length -le 254) + } +} diff --git a/Public/Test-IsUrl.ps1 b/Public/Test-IsUrl.ps1 new file mode 100644 index 0000000..7049b58 --- /dev/null +++ b/Public/Test-IsUrl.ps1 @@ -0,0 +1,55 @@ +function Test-IsUrl { + <# + .SYNOPSIS + Tests absolute HTTP, HTTPS, FTP and FTPS URL syntax without network access. + .DESCRIPTION + Accepts DNS/IDN names, localhost, strict dotted IPv4, bracketed IPv6, optional + numeric ports (0-65535), paths, query strings and fragments. DNS root dots are + accepted. Rejects credentials, whitespace, controls, backslashes, malformed + escapes, IPv4 shorthand and IPv6 scope identifiers. This does not test endpoint + availability or establish that a URL is safe to fetch. + .PARAMETER Url + An absolute URL. Null, empty and malformed values return false. + .EXAMPLE + Test-IsUrl -Url 'https://example.com:8443/api?name=value#section' + .EXAMPLE + 'https://example.com', '/relative' | Test-IsUrl + #> + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [AllowNull()] + [AllowEmptyString()] + [string]$Url + ) + + process { + if ([string]::IsNullOrWhiteSpace($Url) -or $Url -match '[\s\p{Cc}\\]') { return $false } + [System.Uri]$parsed = $null + if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$parsed)) { return $false } + if ($parsed.Scheme -notin @('http', 'https', 'ftp', 'ftps') -or + -not $parsed.IsWellFormedOriginalString() -or $parsed.UserInfo.Length -gt 0) { + return $false + } + + # Validate the original authority: Uri can normalize IPv4 shorthand and an empty port. + if ($Url -notmatch '^[a-zA-Z]+://([^/?#]+)') { return $false } + $authority = $Matches[1] + if ($authority -notmatch '^(?\[[^\]]+\]|[^:]+)(?::(?[0-9]+))?$') { return $false } + $hostText = $Matches['host'] + $portText = $Matches['port'] + if ($portText) { + [int]$port = 0 + if (-not [int]::TryParse($portText, [ref]$port) -or $port -gt 65535) { return $false } + } + if ($hostText.StartsWith('[')) { + $ip = $hostText.Substring(1, $hostText.Length - 2) + return ($ip.Contains(':') -and -not $ip.Contains('%') -and (Test-IsIP -IP $ip)) + } + if ($parsed.HostNameType -eq [System.UriHostNameType]::IPv4 -or $hostText -match '^[0-9.]+$') { + return (Test-IsIP -IP $hostText) + } + return (Test-ITToolBoxDnsName -Name $hostText -AllowRootDot) + } +} diff --git a/README.md b/README.md index 1c6e040..ac99847 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ The foundation imports without WinSCP, GnuPG, Active Directory or Exchange depen | Test-IsValidPath | Native filesystem path syntax; no existence check | | Test-IsIP | Standard IPv4/IPv6 literals; strict dotted IPv4 | | Test-IsDate | Culture-aware date validation with optional exact format | +| Test-IsEmail | Common bare email-address syntax, including IDN domains | +| Test-IsUrl | Absolute HTTP/HTTPS/FTP/FTPS URL syntax | | New-StringEncryption | Passphrase-based AES-256-GCM string encryption | | New-StringDecryption | Authenticate and decrypt the versioned string format | | New-RandomString | Secure random selection from the historical alphabet | @@ -58,10 +60,10 @@ Redaction is opt-in and does not guarantee detection of every secret. - SCP and GnuPG wrappers and bundled WinSCP binaries are removed. Separate modules will own file transfer and OpenPGP; no replacement is bundled here. - `Legacy/` retains string encryption, Exchange and script-context helpers for reference. -- `Staging/v3/` retains 9 candidate commands pending tests and compatibility fixes. - These include existing validators, strings, password generation, API requests, - registry, uptime and AD utilities. They are not currently exported. -- Only the eighteen listed commands are exported. Private helpers, variables and aliases +- `Staging/v3/` retains 7 candidate commands pending tests and compatibility fixes. + These cover logon timestamps, uptime, report chains, character removal, + distinguished names, user principal names and registry values. They are not currently exported. +- Only the twenty listed commands are exported. Private helpers, variables and aliases are not exported. Existing calls to other v2 commands require the v2 release until those commands return to the supported API. - The module GUID and Git history are preserved. @@ -97,6 +99,34 @@ filename. IPv4 shorthand accepted by .NET is deliberately rejected. `Test-IsDate` defaults to the current culture. For deterministic input, use `Test-IsDate '2026-10-04' -Format 'yyyy-MM-dd' -Culture ''` (invariant culture). +`Test-IsEmail` validates a practical subset of bare mailbox syntax: ASCII dot-atom +local parts (including plus tags) and DNS domains, including IDNs and single-label +names. Limits are 64 characters for the local part, 63 ASCII characters per domain +label and 254 characters for the address after IDN conversion. It intentionally +rejects display names, comments, quoted or Unicode local parts, address literals, +whitespace and controls. It is not a complete RFC email validator and does not +check mailbox existence, DNS or deliverability. The `Email`, `Mail` and `Address` +parameter aliases remain available. + +`Test-IsUrl` accepts absolute HTTP, HTTPS, FTP and FTPS URLs with DNS/IDN hosts, +localhost, strict dotted IPv4 or bracketed IPv6, optional numeric ports 0–65535, +paths, queries and fragments. DNS root dots are allowed. It rejects credentials, +relative URLs, other schemes, raw whitespace/controls, backslashes, malformed +percent escapes, empty/invalid ports, IPv4 shorthand and IPv6 scope identifiers. +It checks syntax only, not endpoint reachability or whether fetching a URL is safe. + +```powershell +'person+tag@example.com', 'bad' | Test-IsEmail +'https://example.com:8443/api?name=value', '/relative' | Test-IsUrl +``` + +Both commands return one boolean per pipeline input; explicitly supplied null, +empty or malformed input returns false. Migration from the staged versions: +email validation no longer accepts a display-name/comment wrapper, and the URL +regex is replaced with structured parsing and host checks. FTP and FTPS remain +supported for compatibility; this does not introduce a file-transfer command. + + ## String encryption The modern commands retain their names but intentionally change the encryption diff --git a/Staging/v3/README.md b/Staging/v3/README.md index 47636cd..368d5de 100644 --- a/Staging/v3/README.md +++ b/Staging/v3/README.md @@ -2,5 +2,6 @@ These existing utilities are retained unchanged but are not loaded or exported. They return to Public only after behavioral tests and necessary PowerShell 7 fixes. -This includes API, password, string, validation, Windows uptime, registry and AD utilities. +Remaining candidates cover logon timestamps, uptime, report chains, character removal, +distinguished names, user principal names and registry values. This folder is not a supported API and must not be dot-sourced as part of normal module import. diff --git a/Staging/v3/Test-IsEmail.ps1 b/Staging/v3/Test-IsEmail.ps1 deleted file mode 100644 index 8c4bdac..0000000 --- a/Staging/v3/Test-IsEmail.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -function Test-IsEmail -{ - <# - .SYNOPSIS - Function to check if a string is an RFC email address. - - .DESCRIPTION - Function will check if an input string is an RFC complient email address. - - .PARAMETER EmailAddress - A string representing the email address to be checked - - .EXAMPLE - PS C:\> Test-IsEmail -EmailAddress 'value1' - - .OUTPUTS - System.Boolean - - .LINK - Restrictions on email addresses - https://tools.ietf.org/html/rfc3696#section-3 - #> - - [OutputType([bool])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [Alias('Email', 'Mail', 'Address')] - [string] - $EmailAddress - ) - - try - { - # Check if address is RFC compliant - [void]([mailaddress]$EmailAddress) - - Write-Verbose -Message "Address $EmailAddress is an RFC compliant address" - - return $true - } - catch - { - Write-Verbose -Message "Address $EmailAddress is not an RFC compliant address" - - return $false - } -} \ No newline at end of file diff --git a/Staging/v3/Test-IsUrl.ps1 b/Staging/v3/Test-IsUrl.ps1 deleted file mode 100644 index 4ee401a..0000000 --- a/Staging/v3/Test-IsUrl.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -function Test-IsURL -{ - <# - .SYNOPSIS - Tests if input is an URL - - .DESCRIPTION - Tests if input is an URL - - .PARAMETER Url - A string containing an URL address to check - - .OUTPUTS - System.Boolean - #> - - [OutputType([Boolean])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] - $Url - ) - - # Check we have something - if ([string]::IsNullOrEmpty($Url) -eq $true) - { - return $false - } - - # Check Url is valid - return $Url -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$" -} \ No newline at end of file diff --git a/Tests/EmailUrlValidation.Tests.ps1 b/Tests/EmailUrlValidation.Tests.ps1 new file mode 100644 index 0000000..eba5363 --- /dev/null +++ b/Tests/EmailUrlValidation.Tests.ps1 @@ -0,0 +1,113 @@ +BeforeAll { + Import-Module (Join-Path $PSScriptRoot '../IT-ToolBox.psd1') -Force -ErrorAction Stop +} + +Describe 'Bare email-address validation' { + It 'accepts a common bare address: ' -ForEach @( + @{ Value = 'person@example.com' } + @{ Value = 'Person.Name+tag@EXAMPLE.COM' } + @{ Value = "a!#$%&'*+-/=?^_``{|}~@example.com" } + @{ Value = 'person@localhost' } + @{ Value = 'person@bücher.example' } + @{ Value = 'person@xn--bcher-kva.example' } + @{ Value = 'person@sub-domain.example' } + ) { + Test-IsEmail -EmailAddress $Value | Should -BeTrue + } + + It 'rejects a malformed or unsupported address: ' -ForEach @( + @{ Value = $null }; @{ Value = '' }; @{ Value = ' ' } + @{ Value = 'not-an-email' }; @{ Value = 'person@' }; @{ Value = '@example.com' } + @{ Value = ' person@example.com' }; @{ Value = 'person@example.com ' } + @{ Value = "person@example.com`r`nBcc: other@example.com" } + @{ Value = "per$([char]0)son@example.com" } + @{ Value = 'Person ' }; @{ Value = '' } + @{ Value = 'person(comment)@example.com' }; @{ Value = 'person@example.com(comment)' } + @{ Value = 'a@example.com,b@example.com' }; @{ Value = 'a@example.com;b@example.com' } + @{ Value = 'a..b@example.com' }; @{ Value = '.person@example.com' }; @{ Value = 'person.@example.com' } + @{ Value = '"person name"@example.com' }; @{ Value = 'pérson@example.com' } + @{ Value = 'person@[127.0.0.1]' }; @{ Value = 'person@[IPv6:::1]' } + @{ Value = 'person@-bad.example' }; @{ Value = 'person@bad-.example' } + @{ Value = 'person@bad_domain.example' }; @{ Value = 'person@example..com' } + @{ Value = 'person@example.com.' }; @{ Value = 'person@bad/example.com' } + ) { + Test-IsEmail -EmailAddress $Value | Should -BeFalse + } + + It 'preserves the Email, Mail and Address parameter aliases' { + Test-IsEmail -Email 'person@example.com' | Should -BeTrue + Test-IsEmail -Mail 'person@example.com' | Should -BeTrue + Test-IsEmail -Address 'person@example.com' | Should -BeTrue + } + + It 'returns one boolean per pipeline record, including blank input' { + $results = @('person@example.com', '', 'bad', 'person@localhost') | Test-IsEmail + $results.Count | Should -Be 4 + ($results -join ',') | Should -Be 'True,False,False,True' + foreach ($result in $results) { $result | Should -BeOfType ([bool]) } + } + + It 'enforces local-part, DNS-label and complete-address length boundaries' { + Test-IsEmail (('a' * 64) + '@example.com') | Should -BeTrue + Test-IsEmail (('a' * 65) + '@example.com') | Should -BeFalse + Test-IsEmail ('a@' + ('b' * 63) + '.example') | Should -BeTrue + Test-IsEmail ('a@' + ('b' * 64) + '.example') | Should -BeFalse + $domain = ('b' * 63) + '.' + ('c' * 63) + '.' + ('d' * 61) + Test-IsEmail (('a' * 64) + '@' + $domain) | Should -BeTrue + Test-IsEmail (('a' * 64) + '@' + $domain + 'e') | Should -BeFalse + # IDN length is measured after conversion, not from the Unicode spelling. + Test-IsEmail ('a@' + ('é' * 60) + '.example') | Should -BeFalse + } +} + +Describe 'Absolute network URL validation' { + It 'accepts supported absolute URL syntax: ' -ForEach @( + @{ Value = 'https://example.com' }; @{ Value = 'HTTP://EXAMPLE.COM' } + @{ Value = 'ftp://example.com/file.txt' }; @{ Value = 'ftps://example.com:990/file.txt' } + @{ Value = 'https://example.com:8443/api?name=value&limit=10#section' } + @{ Value = 'https://example.com:0/' }; @{ Value = 'https://example.com:65535/' } + @{ Value = 'http://localhost:8080/health' }; @{ Value = 'http://192.168.1.10/' } + @{ Value = 'https://[2001:db8::1]:443/path' }; @{ Value = 'http://[::1]:8080/' } + @{ Value = 'http://[::ffff:192.168.1.10]/' } + @{ Value = 'https://bücher.example/über' }; @{ Value = 'https://xn--bcher-kva.example/' } + @{ Value = 'https://example.com./' }; @{ Value = 'https://example.com/a%20b?q=%2F' } + ) { + Test-IsUrl -Url $Value | Should -BeTrue + } + + It 'rejects malformed or unsupported URL syntax: ' -ForEach @( + @{ Value = $null }; @{ Value = '' }; @{ Value = ' ' } + @{ Value = '/relative/path' }; @{ Value = 'example.com' }; @{ Value = '//example.com/path' } + @{ Value = 'mailto:person@example.com' }; @{ Value = 'file:///tmp/file' } + @{ Value = 'ssh://example.com' }; @{ Value = 'javascript:alert(1)' } + @{ Value = 'https://' }; @{ Value = 'https:///example.com' } + @{ Value = ' https://example.com' }; @{ Value = 'https://example.com ' } + @{ Value = 'https://example.com/a b' }; @{ Value = "https://example.com/`npath" } + @{ Value = "https://example.com/$([char]0)" }; @{ Value = 'https://example.com\path' } + @{ Value = 'https://example.com/%zz' }; @{ Value = 'https://example.com/%' } + @{ Value = 'https://user:password@example.com/' }; @{ Value = 'ftp://user@example.com/file' } + @{ Value = 'https://@example.com/' } + @{ Value = 'https://example.com:' }; @{ Value = 'https://example.com:abc/' } + @{ Value = 'https://example.com:-1/' }; @{ Value = 'https://example.com:65536/' } + @{ Value = 'https://256.1.1.1/' }; @{ Value = 'https://127.1/' } + @{ Value = 'https://0x7f000001/' }; @{ Value = 'https://01.2.3.4/' } + @{ Value = 'https://[2001:db8:::1]/' }; @{ Value = 'https://[fe80::1%251]/' } + @{ Value = 'https://[example.com]/' }; @{ Value = 'https://[127.0.0.1]/' } + @{ Value = 'https://-bad.example/' }; @{ Value = 'https://bad-.example/' } + @{ Value = 'https://bad_domain.example/' }; @{ Value = 'https://example..com/' } + ) { + Test-IsUrl -Url $Value | Should -BeFalse + } + + It 'returns one boolean per pipeline record and preserves case-insensitive command calls' { + $results = @('https://example.com', '', '/relative', 'ftp://localhost/file') | Test-IsURL + $results.Count | Should -Be 4 + ($results -join ',') | Should -Be 'True,False,False,True' + foreach ($result in $results) { $result | Should -BeOfType ([bool]) } + } + + It 'enforces DNS label boundaries for URL hosts' { + Test-IsUrl ('https://' + ('a' * 63) + '.example/') | Should -BeTrue + Test-IsUrl ('https://' + ('a' * 64) + '.example/') | Should -BeFalse + } +} diff --git a/Tests/Module.Tests.ps1 b/Tests/Module.Tests.ps1 index 2469d4a..123849c 100644 --- a/Tests/Module.Tests.ps1 +++ b/Tests/Module.Tests.ps1 @@ -10,7 +10,7 @@ Describe 'IT-ToolBox module boundary' { } It 'exports exactly the supported commands' { - $expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsDate', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object + $expected = @('New-LogEntry', 'New-Timer', 'Get-TimerStatus', 'Stop-Timer', 'Get-ElapsedTime', 'Test-FileName', 'Test-IsValidPath', 'Test-IsIP', 'Test-IsDate', 'Test-IsEmail', 'Test-IsUrl', 'New-StringEncryption', 'New-StringDecryption', 'New-RandomString', 'New-RandomPassword', 'New-PhoneticPassword', 'New-ApiRequest', 'New-StringConversion', 'Get-StringCheckSum', 'Get-StringHashCode') | Sort-Object $actual = @($module.ExportedFunctions.Keys | Sort-Object) ($actual -join ',') | Should -Be ($expected -join ',') $module.ExportedVariables.Count | Should -Be 0 @@ -20,6 +20,7 @@ Describe 'IT-ToolBox module boundary' { It 'keeps logging helpers private' { Get-Command Get-NewLogEntryMutexName -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty + Get-Command Test-ITToolBoxDnsName -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty Get-Command Initialize-NewLogEntryState -Module IT-ToolBox -ErrorAction SilentlyContinue | Should -BeNullOrEmpty }