Fix Win11 Creator product-key validation failures (#4791)

* feat: Enhance Win11 Creator with edition ID handling and update autounattend.xml generation

* Strengthen Pester coverage for config and function files
This commit is contained in:
Chris Titus
2026-07-01 17:15:26 -05:00
committed by GitHub
parent 8062815655
commit 104c763132
6 changed files with 406 additions and 108 deletions
+96 -69
View File
@@ -1,83 +1,110 @@
# Import Config Files
$global:importedconfigs = @{}
Get-ChildItem .\config | Where-Object {$_.Extension -eq ".json"} | ForEach-Object {
$global:importedconfigs[$psitem.BaseName] = Get-Content $psitem.FullName | ConvertFrom-Json
}
#===========================================================================
# Tests - Application Installs
# Tests - Config Files
#===========================================================================
Describe "Config Files" -ForEach @(
@{
name = "applications"
config = $('{
"winget": "value",
"choco": "value",
"category": "value",
"content": "value",
"description": "value",
"link": "value"
}' | ConvertFrom-Json)
},
@{
name = "tweaks"
undo = $true
}
) {
Context "$name config file" {
It "Imports with no errors" {
$global:importedconfigs.$name | should -Not -BeNullOrEmpty
$configRoot = Join-Path $PSScriptRoot "..\config"
$configCases = @(
Get-ChildItem -Path $configRoot -Filter *.json | ForEach-Object {
@{
Name = $_.Name
Path = $_.FullName
}
if ($config) {
It "Imports should be the correct structure" {
$applications = $global:importedconfigs.$name | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name
$template = $config | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name
$result = New-Object System.Collections.Generic.List[System.Object]
Foreach ($application in $applications) {
$compare = $global:importedconfigs.$name.$application | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name
if (-not $compare) {
throw "Comparison object for application '$application' is null."
}
if (-not $template) {
throw "Template object for application '$application' is null."
}
if ($(Compare-Object $compare $template) -ne $null) {
$result.Add($application)
}
}
}
)
$result | Select-String "WPF*" | should -BeNullOrEmpty
Describe "Config files" {
foreach ($configCase in $configCases) {
It "imports $($configCase.Name) with no JSON errors" -TestCases $configCase {
param([string]$Name, [string]$Path)
try {
Get-Content -Path $Path -Raw | ConvertFrom-Json | Out-Null
} catch {
throw "Failed to import ${Name}: $_"
}
}
if($undo) {
It "Tweaks should contain original Value" {
$tweaks = $global:importedconfigs.$name | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty name
$result = New-Object System.Collections.Generic.List[System.Object]
}
}
foreach ($tweak in $tweaks) {
$Originals = @(
@{
name = "registry"
value = "OriginalValue"
},
@{
name = "service"
value = "OriginalType"
}
)
Foreach ($original in $Originals) {
$TotalCount = ($global:importedconfigs.$name.$tweak.$($original.name)).count
$OriginalCount = ($global:importedconfigs.$name.$tweak.$($original.name).$($original.value) | Where-Object {$_}).count
if($TotalCount -ne $OriginalCount) {
$result.Add("$Tweak,$($original.name)")
}
}
Describe "Applications config" {
$testCase = @{ Path = (Join-Path $configRoot "applications.json") }
It "contains at least one application" -TestCases $testCase {
param([string]$Path)
$applications = Get-Content -Path $Path -Raw | ConvertFrom-Json
$applicationEntries = @($applications.PSObject.Properties)
if ($applicationEntries.Count -eq 0) {
throw "applications.json does not contain any application entries."
}
}
It "contains required display fields and at least one install source" -TestCases $testCase {
param([string]$Path)
$applications = Get-Content -Path $Path -Raw | ConvertFrom-Json
$requiredFields = @("category", "content", "description", "link")
$invalidEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $applications.PSObject.Properties) {
$entryFields = @($entry.Value.PSObject.Properties.Name)
foreach ($field in $requiredFields) {
if ($entryFields -notcontains $field -or [string]::IsNullOrWhiteSpace([string]$entry.Value.$field)) {
$invalidEntries.Add("$($entry.Name) missing $field")
}
$result | Select-String "WPF*" | should -BeNullOrEmpty
}
$hasInstallSource = $false
foreach ($sourceField in @("winget", "choco")) {
if ($entryFields -contains $sourceField -and -not [string]::IsNullOrWhiteSpace([string]$entry.Value.$sourceField)) {
$hasInstallSource = $true
}
}
if (-not $hasInstallSource) {
$invalidEntries.Add("$($entry.Name) missing winget/choco install source")
}
}
if ($invalidEntries.Count -gt 0) {
throw ($invalidEntries -join "`n")
}
}
}
Describe "Tweaks config" {
$testCase = @{ Path = (Join-Path $configRoot "tweaks.json") }
It "contains undo metadata for registry and service actions" -TestCases $testCase {
param([string]$Path)
$tweaks = Get-Content -Path $Path -Raw | ConvertFrom-Json
$invalidTweaks = New-Object System.Collections.Generic.List[string]
foreach ($tweak in $tweaks.PSObject.Properties) {
foreach ($registryEntry in @($tweak.Value.registry)) {
if ($null -eq $registryEntry) { continue }
if ($registryEntry.PSObject.Properties.Name -notcontains "OriginalValue" -or
[string]::IsNullOrWhiteSpace([string]$registryEntry.OriginalValue)) {
$invalidTweaks.Add("$($tweak.Name),registry")
}
}
foreach ($serviceEntry in @($tweak.Value.service)) {
if ($null -eq $serviceEntry) { continue }
if ($serviceEntry.PSObject.Properties.Name -notcontains "OriginalType" -or
[string]::IsNullOrWhiteSpace([string]$serviceEntry.OriginalType)) {
$invalidTweaks.Add("$($tweak.Name),service")
}
}
}
if ($invalidTweaks.Count -gt 0) {
throw ($invalidTweaks -join "`n")
}
}
}
+54 -36
View File
@@ -1,53 +1,71 @@
#===========================================================================
# Tests - Functions
#===========================================================================
Describe "Comprehensive Checks for PS1 Files in Functions Folder" {
BeforeAll {
# Get all .ps1 files in the functions folder
$ps1Files = Get-ChildItem -Path ./functions -Filter *.ps1 -Recurse
$functionRoot = Join-Path $PSScriptRoot "..\functions"
$functionCases = @(
Get-ChildItem -Path $functionRoot -Filter *.ps1 -Recurse | ForEach-Object {
@{
Name = $_.Name
Path = $_.FullName
}
}
)
foreach ($file in $ps1Files) {
Context "Checking $($file.Name)" {
It "Should import without errors" {
{ . $file.FullName } | Should -Not -Throw
}
Describe "Function source files" {
foreach ($functionCase in $functionCases) {
Context "Checking $($functionCase.Path)" {
It "has no parser errors" -TestCases $functionCase {
param([string]$Path)
It "Should have no syntax errors" {
$tokens = $null
$syntaxErrors = $null
$null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Path $file.FullName -Raw), [ref]$syntaxErrors)
$syntaxErrors.Count | Should -Be 0
}
[System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null
It "Should not use deprecated cmdlets or aliases" {
$content = Get-Content -Path $file.FullName -Raw
# Example check for a known deprecated cmdlet or alias
$content | Should -Not -Match 'DeprecatedCmdlet'
# Add more checks as needed
}
It "Should follow naming conventions for functions" {
$functions = (Get-Command -Path $file.FullName).Name
foreach ($function in $functions) {
$function | Should -Match '^[a-z]+(-[a-z]+)*$' # Enforce lower-kebab-case
if ($syntaxErrors.Count -ne 0) {
throw ($syntaxErrors | Out-String)
}
}
It "Should define mandatory parameters for all functions" {
. $file.FullName
$functions = (Get-Command -Path $file.FullName).Name
foreach ($function in $functions) {
$parameters = (Get-Command -Name $function).Parameters.Values
$mandatoryParams = $parameters | Where-Object { $_.Attributes.Mandatory -eq $true }
$mandatoryParams.Count | Should -BeGreaterThan 0
It "defines top-level functions with approved verb-noun names" -TestCases $functionCase {
param([string]$Path)
$tokens = $null
$syntaxErrors = $null
$approvedVerbs = (Get-Verb).Verb
$ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$syntaxErrors)
if ($syntaxErrors.Count -ne 0) {
throw ($syntaxErrors | Out-String)
}
$topLevelFunctions = @(
$ast.EndBlock.Statements |
Where-Object { $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] }
)
if ($topLevelFunctions.Count -eq 0) {
throw "No top-level function was found in $Path."
}
foreach ($function in $topLevelFunctions) {
if ($function.Name -notmatch '^[A-Za-z]+-[A-Za-z0-9]+$') {
throw "Function '$($function.Name)' does not use Verb-Noun naming."
}
$verb = ($function.Name -split '-', 2)[0]
if ($approvedVerbs -notcontains $verb) {
throw "Function '$($function.Name)' does not use an approved PowerShell verb."
}
}
}
It "Should have all functions available after import" {
. $file.FullName
$functions = (Get-Command -Path $file.FullName).Name
foreach ($function in $functions) {
{ Get-Command -Name $function -CommandType Function } | Should -Not -BeNullOrEmpty
It "imports without throwing" -TestCases $functionCase {
param([string]$Path)
try {
. $Path
} catch {
throw "Failed to import ${Path}: $_"
}
}
}
+33
View File
@@ -0,0 +1,33 @@
#===========================================================================
# Tests - Win11 Creator
#===========================================================================
Describe "Win11 Creator setup media" {
It "autounattend template does not force a product key" {
$templatePath = Join-Path $PSScriptRoot "..\tools\autounattend.xml"
[xml]$xml = Get-Content -Path $templatePath -Raw
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMgr.AddNamespace("u", "urn:schemas-microsoft-com:unattend")
$productKeyCount = $xml.SelectNodes("//u:ProductKey", $nsMgr).Count
if ($productKeyCount -ne 0) {
throw "Expected no ProductKey nodes, found $productKeyCount."
}
}
It "ISO script accepts selected edition setup metadata" {
$isoScriptPath = Join-Path $PSScriptRoot "..\functions\private\Invoke-WinUtilISOScript.ps1"
$content = Get-Content -Path $isoScriptPath -Raw
foreach ($pattern in @(
'\[string\]\$InstallEditionId',
'\[int\]\$InstallImageIndex',
'sources\\ei\.cfg',
'PID\.txt'
)) {
if ($content -notmatch $pattern) {
throw "Expected Invoke-WinUtilISOScript.ps1 to match pattern: $pattern"
}
}
}
}