Create winoneshot-compat.Tests.ps1

This commit is contained in:
Chris Titus
2026-08-09 12:27:07 -05:00
parent 8d3adb599f
commit 5ad168e0c4
+353
View File
@@ -0,0 +1,353 @@
#===========================================================================
# Tests - WinOneShot Catalog Compatibility
#===========================================================================
# WinOneShot downloads these files from WinUtil's main/config raw GitHub URLs.
# Keep the known field types aligned with WinOneShot Models.cs; extra fields are allowed.
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$catalogCases = @(
@{
Name = "applications.json"
Path = Join-Path $repoRoot "config\applications.json"
ScalarFields = @{
category = "String"
choco = "String"
content = "String"
description = "String"
foss = "Boolean"
link = "String"
winget = "String"
}
ArrayFields = @{}
}
@{
Name = "appx.json"
Path = Join-Path $repoRoot "config\appx.json"
ScalarFields = @{
Category = "String"
Content = "String"
Description = "String"
Panel = "String"
PackageId = "String"
StoreId = "String"
}
ArrayFields = @{}
}
@{
Name = "feature.json"
Path = Join-Path $repoRoot "config\feature.json"
ScalarFields = @{
Content = "String"
Description = "String"
category = "String"
panel = "String"
Order = "String"
link = "String"
Type = "String"
ButtonWidth = "String"
function = "String"
}
ArrayFields = @{
feature = "StringArray"
InvokeScript = "StringArray"
}
}
@{
Name = "tweaks.json"
Path = Join-Path $repoRoot "config\tweaks.json"
ScalarFields = @{
Content = "String"
Description = "String"
category = "String"
panel = "String"
Order = "String"
Type = "String"
Checked = "String"
ButtonWidth = "String"
ComboItems = "String"
link = "String"
}
ArrayFields = @{
registry = "RegistryArray"
service = "ServiceArray"
ScheduledTask = "ScheduledTaskArray"
appx = "StringArray"
InvokeScript = "StringArray"
UndoScript = "StringArray"
}
}
)
$invalidCatalogCases = @(
@{
Name = "applications.json"
Json = '[{"App":{}}]'
ScalarFields = $catalogCases[0].ScalarFields
ArrayFields = $catalogCases[0].ArrayFields
ExpectedError = "must contain a JSON object"
}
@{
Name = "applications.json"
Json = "{}"
ScalarFields = $catalogCases[0].ScalarFields
ArrayFields = $catalogCases[0].ArrayFields
ExpectedError = "does not contain any entries"
}
@{
Name = "applications.json"
Json = '{"Broken":null}'
ScalarFields = $catalogCases[0].ScalarFields
ArrayFields = $catalogCases[0].ArrayFields
ExpectedError = "applications.json.Broken is null"
}
@{
Name = "applications.json"
Json = '{"App":{"foss":"yes"}}'
ScalarFields = $catalogCases[0].ScalarFields
ArrayFields = $catalogCases[0].ArrayFields
ExpectedError = "foss must be a Boolean"
}
@{
Name = "appx.json"
Json = '{"App":{"PackageId":[]}}'
ScalarFields = $catalogCases[1].ScalarFields
ArrayFields = $catalogCases[1].ArrayFields
ExpectedError = "PackageId must be a string or null"
}
@{
Name = "feature.json"
Json = '{"Feature":{"feature":"Example-Feature"}}'
ScalarFields = $catalogCases[2].ScalarFields
ArrayFields = $catalogCases[2].ArrayFields
ExpectedError = "feature must be an array or null"
}
@{
Name = "tweaks.json"
Json = '{"Tweak":{"registry":{}}}'
ScalarFields = $catalogCases[3].ScalarFields
ArrayFields = $catalogCases[3].ArrayFields
ExpectedError = "registry must be an array or null"
}
)
BeforeAll {
function script:Get-WinOneShotJsonKind {
param($Value)
if ($null -eq $Value) {
return "Null"
}
if ($Value -is [string]) {
return "String"
}
if ($Value -is [bool]) {
return "Boolean"
}
if ($Value -is [array]) {
return "Array"
}
if ($Value -is [pscustomobject]) {
return "Object"
}
return $Value.GetType().Name
}
function script:Get-WinOneShotProperty {
param(
[Parameter(Mandatory)]
[pscustomobject]$Object,
[Parameter(Mandatory)]
[string]$Name
)
foreach ($property in $Object.PSObject.Properties) {
if ($property.Name -ieq $Name) {
$property
}
}
}
function script:Get-WinOneShotTypeError {
param(
[Parameter(Mandatory)]
[string]$Location,
$Value,
[Parameter(Mandatory)]
[string]$ExpectedType
)
$actualKind = Get-WinOneShotJsonKind -Value $Value
if ($ExpectedType -eq "String") {
if ($actualKind -notin @("String", "Null")) {
return "$Location must be a string or null; found $actualKind"
}
return
}
if ($ExpectedType -eq "Boolean") {
if ($actualKind -ne "Boolean") {
return "$Location must be a Boolean; found $actualKind"
}
return
}
if ($actualKind -eq "Null") {
return
}
if ($actualKind -ne "Array") {
return "$Location must be an array or null; found $actualKind"
}
if ($ExpectedType -eq "StringArray") {
for ($index = 0; $index -lt $Value.Count; $index++) {
$elementKind = Get-WinOneShotJsonKind -Value $Value[$index]
if ($elementKind -notin @("String", "Null")) {
"$Location[$index] must be a string or null; found $elementKind"
}
}
return
}
$nestedFields = switch ($ExpectedType) {
"RegistryArray" {
@("Path", "Name", "Value", "OriginalValue", "DefaultState", "Type")
}
"ServiceArray" {
@("Name", "StartupType", "OriginalType")
}
"ScheduledTaskArray" {
@("Name")
}
default {
throw "Unknown WinOneShot contract type: $ExpectedType"
}
}
for ($index = 0; $index -lt $Value.Count; $index++) {
$element = $Value[$index]
$elementKind = Get-WinOneShotJsonKind -Value $element
if ($elementKind -eq "Null") {
continue
}
if ($elementKind -ne "Object") {
"$Location[$index] must be an object or null; found $elementKind"
continue
}
foreach ($field in $nestedFields) {
foreach ($property in (Get-WinOneShotProperty -Object $element -Name $field)) {
$propertyKind = Get-WinOneShotJsonKind -Value $property.Value
if ($propertyKind -notin @("String", "Null")) {
"$Location[$index].$($property.Name) must be a string or null; found $propertyKind"
}
}
}
}
}
function script:Get-WinOneShotCatalogError {
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter(Mandatory)]
[string]$Json,
[Parameter(Mandatory)]
[hashtable]$ScalarFields,
[Parameter(Mandatory)]
[hashtable]$ArrayFields
)
try {
$catalog = $Json | ConvertFrom-Json -NoEnumerate -ErrorAction Stop
} catch {
return "WinOneShot cannot parse ${Name}: $_"
}
if ((Get-WinOneShotJsonKind -Value $catalog) -ne "Object") {
return "$Name must contain a JSON object."
}
$entries = @($catalog.PSObject.Properties)
if ($entries.Count -eq 0) {
return "$Name does not contain any entries."
}
foreach ($entry in $entries) {
$entryKind = Get-WinOneShotJsonKind -Value $entry.Value
if ($entryKind -eq "Null") {
"$Name.$($entry.Name) is null"
continue
}
if ($entryKind -ne "Object") {
"$Name.$($entry.Name) must be an object; found $entryKind"
continue
}
foreach ($field in $ScalarFields.Keys) {
foreach ($property in (Get-WinOneShotProperty -Object $entry.Value -Name $field)) {
Get-WinOneShotTypeError -Location "$Name.$($entry.Name).$($property.Name)" -Value $property.Value -ExpectedType $ScalarFields[$field]
}
}
foreach ($field in $ArrayFields.Keys) {
foreach ($property in (Get-WinOneShotProperty -Object $entry.Value -Name $field)) {
Get-WinOneShotTypeError -Location "$Name.$($entry.Name).$($property.Name)" -Value $property.Value -ExpectedType $ArrayFields[$field]
}
}
}
}
}
Describe "WinOneShot catalog download contract" {
foreach ($catalogCase in $catalogCases) {
It "publishes <Name> at the exact raw-download path" -TestCases $catalogCase {
param([string]$Path)
Test-Path -LiteralPath $Path -PathType Leaf | Should -BeTrue
$exactNameMatch = @(
Get-ChildItem -LiteralPath (Split-Path -Path $Path -Parent) -File |
Where-Object { $_.Name -ceq (Split-Path -Path $Path -Leaf) }
)
$exactNameMatch.Count | Should -Be 1
}
It "loads <Name> with WinOneShot-compatible field types" -TestCases $catalogCase {
param(
[string]$Name,
[string]$Path,
[hashtable]$ScalarFields,
[hashtable]$ArrayFields
)
$json = Get-Content -LiteralPath $Path -Raw
$errors = @(Get-WinOneShotCatalogError -Name $Name -Json $json -ScalarFields $ScalarFields -ArrayFields $ArrayFields)
if ($errors.Count -gt 0) {
throw ($errors -join "`n")
}
}
}
}
Describe "WinOneShot catalog contract guard" {
It "rejects consumer-breaking <Name> fixture: <ExpectedError>" -TestCases $invalidCatalogCases {
param(
[string]$Name,
[string]$Json,
[hashtable]$ScalarFields,
[hashtable]$ArrayFields,
[string]$ExpectedError
)
$errors = @(Get-WinOneShotCatalogError -Name $Name -Json $Json -ScalarFields $ScalarFields -ArrayFields $ArrayFields)
$errors -join "`n" | Should -BeLike "*$ExpectedError*"
}
}