Skip to content

Commit 6b31f7e

Browse files
Merge pull request #68 from k-grube/fix/list-function-parameters-timeout
fix: restore function-parameters.json generation for ListFunctionParameters Synced from CyberDrain/CIPP@a1e479e
1 parent 0a8b617 commit 6b31f7e

2 files changed

Lines changed: 295 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Pester tests for build/tools/build-function-parameters.ps1
2+
# Generates a fixture module, runs the generator, dot-sources the same fixture and
3+
# asserts the synthesized synopses match live Get-Help byte for byte. Covers the
4+
# constructs that broke earlier revisions: mixed explicit positions, named parameter
5+
# sets, ghost default sets, positioned switches, non-$true mandatory literals.
6+
7+
BeforeAll {
8+
$RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
9+
$GeneratorPath = Join-Path (Split-Path -Parent $RepoRoot) 'build/tools/build-function-parameters.ps1'
10+
if (-not (Test-Path $GeneratorPath)) { throw "Could not locate build-function-parameters.ps1 at $GeneratorPath" }
11+
12+
$FixtureRoot = Join-Path $TestDrive 'FixtureModule'
13+
$PublicDir = Join-Path $FixtureRoot 'Public'
14+
$null = New-Item -ItemType Directory -Path $PublicDir -Force
15+
16+
$FixtureSource = @'
17+
function Test-AutoPositional {
18+
param($User, [string]$Name, [switch]$Force)
19+
}
20+
function Test-MandatoryForms {
21+
param(
22+
[Parameter(Mandatory)][string]$FlagForm,
23+
[Parameter(Mandatory = $true)]$ExplicitTrue,
24+
[Parameter(Mandatory = 1)][string]$NumericTrue,
25+
[Parameter(Mandatory = $false)]$ExplicitFalse
26+
)
27+
}
28+
function Test-MixedPositions {
29+
[CmdletBinding()]
30+
param(
31+
[Parameter(Position = 1)][string]$Second,
32+
[Parameter(Position = 0)][string]$First,
33+
[Parameter()][string]$Named
34+
)
35+
}
36+
function Test-NamedSets {
37+
[CmdletBinding()]
38+
param(
39+
[Parameter(ParameterSetName = 'Single', Mandatory)][string]$UserId,
40+
[Parameter(ParameterSetName = 'Single')][string]$Nickname,
41+
[Parameter(ParameterSetName = 'Bulk', Mandatory)][System.Collections.Generic.List[object]]$Requests,
42+
[Parameter(Mandatory)][string]$TenantFilter,
43+
$Headers
44+
)
45+
}
46+
function Test-SwitchInNamedSet {
47+
[CmdletBinding()]
48+
param(
49+
[Parameter(ParameterSetName = 'A')][switch]$Sw,
50+
[Parameter(ParameterSetName = 'A')][string]$S
51+
)
52+
}
53+
function Test-PositionedSwitch {
54+
[CmdletBinding()]
55+
param(
56+
[Parameter(Position = 0)][switch]$Sw,
57+
[Parameter(Position = 1)][string]$Str
58+
)
59+
}
60+
function Test-GhostDefault {
61+
[CmdletBinding(DefaultParameterSetName = 'Nope')]
62+
param(
63+
[Parameter(ParameterSetName = 'A')][string]$X,
64+
[Parameter(ParameterSetName = 'B')][string]$Y
65+
)
66+
}
67+
function Test-ShouldProcess {
68+
[CmdletBinding(SupportsShouldProcess = $true)]
69+
param([string]$Target)
70+
}
71+
function Test-WithCommentHelp {
72+
<#
73+
.SYNOPSIS
74+
a real synopsis
75+
.FUNCTIONALITY
76+
Internal
77+
.PARAMETER Thing
78+
the thing
79+
#>
80+
param([string]$Thing)
81+
}
82+
'@
83+
Set-Content -Path (Join-Path $PublicDir 'fixtures.ps1') -Value $FixtureSource
84+
85+
$OutputPath = Join-Path $TestDrive 'out.json'
86+
& $GeneratorPath -ModulePath $FixtureRoot -OutputPath $OutputPath | Out-Null
87+
$script:Generated = Get-Content $OutputPath -Raw | ConvertFrom-Json -AsHashtable
88+
89+
# same definitions live in the session so Get-Help renders its real synopses
90+
. (Join-Path $PublicDir 'fixtures.ps1')
91+
92+
$script:NoHelpFunctions = @(
93+
'Test-AutoPositional', 'Test-MandatoryForms', 'Test-MixedPositions', 'Test-NamedSets',
94+
'Test-SwitchInNamedSet', 'Test-PositionedSwitch', 'Test-GhostDefault', 'Test-ShouldProcess'
95+
)
96+
}
97+
98+
Describe 'build-function-parameters generator' {
99+
It 'synthesizes a synopsis byte-identical to Get-Help for <_>' -ForEach @(
100+
'Test-AutoPositional', 'Test-MandatoryForms', 'Test-MixedPositions', 'Test-NamedSets',
101+
'Test-SwitchInNamedSet', 'Test-PositionedSwitch', 'Test-GhostDefault', 'Test-ShouldProcess'
102+
) {
103+
$expected = (Get-Help $_).Synopsis.Trim()
104+
# Get-Help joins multi-set blocks with the platform newline; the cache pins CRLF
105+
$normalizedGenerated = $script:Generated[$_]['Synopsis'] -replace "`r`n", "`n"
106+
$normalizedExpected = $expected -replace "`r`n", "`n"
107+
$normalizedGenerated | Should -BeExactly $normalizedExpected
108+
}
109+
110+
It 'extracts comment-based help instead of synthesizing' {
111+
$script:Generated['Test-WithCommentHelp']['Synopsis'] | Should -Be 'a real synopsis'
112+
$script:Generated['Test-WithCommentHelp']['Functionality'] | Should -Be 'Internal'
113+
($script:Generated['Test-WithCommentHelp']['Parameters'] | Where-Object { $_['Name'] -eq 'Thing' })['Description'] | Should -Be 'the thing'
114+
}
115+
116+
It 'marks non-$true mandatory literals as required' {
117+
$params = @{}
118+
foreach ($p in $script:Generated['Test-MandatoryForms']['Parameters']) { $params[$p['Name']] = $p['Required'] }
119+
$params['FlagForm'] | Should -BeTrue
120+
$params['ExplicitTrue'] | Should -BeTrue
121+
$params['NumericTrue'] | Should -BeTrue
122+
$params['ExplicitFalse'] | Should -BeFalse
123+
}
124+
125+
It 'fails the build when a source file cannot be parsed' {
126+
$brokenRoot = Join-Path $TestDrive 'BrokenModule'
127+
$null = New-Item -ItemType Directory -Path (Join-Path $brokenRoot 'Public') -Force
128+
Set-Content -Path (Join-Path $brokenRoot 'Public/ok.ps1') -Value 'function Get-Ok { param($A) }'
129+
Set-Content -Path (Join-Path $brokenRoot 'Public/broken.ps1') -Value 'function Get-Broken {{{'
130+
{ & $GeneratorPath -ModulePath $brokenRoot -OutputPath (Join-Path $TestDrive 'broken-out.json') } | Should -Throw '*failed to parse*'
131+
}
132+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Pester tests for Invoke-ListFunctionParameters
2+
# Validates the pregenerated cache path (Config/function-parameters.json), the guard
3+
# that skips commands outside the pregenerated set (deployed legacy behavior), the
4+
# live Get-Help fallback when no cache exists, and entrypoint filtering.
5+
6+
BeforeAll {
7+
# Resolve by name under Modules/ so the test survives the function moving between modules.
8+
$RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
9+
$FunctionPath = Get-ChildItem -Path (Join-Path $RepoRoot 'Modules') -Recurse -Filter 'Invoke-ListFunctionParameters.ps1' -File -ErrorAction SilentlyContinue |
10+
Select-Object -First 1 -ExpandProperty FullName
11+
if (-not $FunctionPath) { throw 'Could not locate Invoke-ListFunctionParameters.ps1 under Modules/' }
12+
13+
class HttpResponseContext {
14+
[object]$StatusCode
15+
[object]$Body
16+
}
17+
# The Functions worker exposes [HttpStatusCode]; map it for standalone test runs.
18+
$Accelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
19+
if (-not ('HttpStatusCode' -as [type])) {
20+
$Accelerators::Add('HttpStatusCode', [System.Net.HttpStatusCode])
21+
}
22+
23+
function New-ExoRequest { param($AvailableCmdlets, $tenantid, $NoAuthCheck, $Compliance) }
24+
25+
# fake FunctionInfo, endpoint reads Name / Visibility / Parameters
26+
function New-FakeFunction {
27+
param($Name)
28+
[pscustomobject]@{
29+
Name = $Name
30+
Visibility = 'Public'
31+
Parameters = @{
32+
SomeParam = [pscustomobject]@{
33+
ParameterType = [pscustomobject]@{ FullName = 'System.String' }
34+
Attributes = @([pscustomobject]@{ Mandatory = $true })
35+
}
36+
}
37+
}
38+
}
39+
40+
function New-CacheFile {
41+
param($Root, $Functions)
42+
$configDir = Join-Path $Root 'Config'
43+
$null = New-Item -ItemType Directory -Path $configDir -Force
44+
$Functions | ConvertTo-Json -Depth 8 | Set-Content -Path (Join-Path $configDir 'function-parameters.json')
45+
}
46+
47+
. $FunctionPath
48+
49+
# the function also emits $Results to the pipeline before returning the
50+
# response context, the worker keys on the HttpResponseContext object
51+
function Invoke-Endpoint {
52+
param($Request)
53+
Invoke-ListFunctionParameters -Request $Request -TriggerMetadata $null | Where-Object { $_ -is [HttpResponseContext] } | Select-Object -First 1
54+
}
55+
}
56+
57+
Describe 'Invoke-ListFunctionParameters' {
58+
BeforeEach {
59+
$global:CIPPFunctionParameters = $null
60+
$script:savedRootPath = $env:CIPPRootPath
61+
62+
Mock -CommandName Get-Help -MockWith {
63+
[pscustomobject]@{
64+
Functionality = ''
65+
Synopsis = 'live synopsis'
66+
parameters = [pscustomobject]@{
67+
parameter = @([pscustomobject]@{ name = 'SomeParam'; description = @([pscustomobject]@{ Text = 'live description' }) })
68+
}
69+
}
70+
}
71+
}
72+
73+
AfterEach {
74+
$global:CIPPFunctionParameters = $null
75+
$env:CIPPRootPath = $script:savedRootPath
76+
}
77+
78+
It 'serves cached functions without calling Get-Help' {
79+
$env:CIPPRootPath = Join-Path $TestDrive 'cached'
80+
New-CacheFile -Root $env:CIPPRootPath -Functions @{
81+
'Get-CIPPFoo' = @{
82+
Functionality = ''
83+
Synopsis = 'cached synopsis'
84+
Parameters = @(@{ Name = 'SomeParam'; Type = 'System.String'; Description = 'cached description'; Required = $true })
85+
}
86+
}
87+
Mock -CommandName Get-Command -MockWith { @(New-FakeFunction -Name 'Get-CIPPFoo') }
88+
89+
$request = [pscustomobject]@{ Query = [pscustomobject]@{ Module = 'CIPPCore' } }
90+
$response = Invoke-Endpoint -Request $request
91+
92+
$response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK)
93+
$response.Body | Should -HaveCount 1
94+
$response.Body[0].Function | Should -Be 'Get-CIPPFoo'
95+
$response.Body[0].Synopsis | Should -Be 'cached synopsis'
96+
$response.Body[0].Parameters[0].Description | Should -Be 'cached description'
97+
Should -Invoke Get-Help -Times 0 -Exactly
98+
}
99+
100+
It 'skips functions outside the pregenerated set without calling Get-Help' {
101+
$env:CIPPRootPath = Join-Path $TestDrive 'partial'
102+
New-CacheFile -Root $env:CIPPRootPath -Functions @{
103+
'Get-CIPPFoo' = @{
104+
Functionality = ''
105+
Synopsis = 'cached synopsis'
106+
Parameters = @(@{ Name = 'SomeParam'; Type = 'System.String'; Description = 'cached description'; Required = $true })
107+
}
108+
}
109+
# cache present -> uncached commands are guard-skipped, never Get-Help'd
110+
# (a bare Get-Command can return every command in the runspace)
111+
Mock -CommandName Get-Command -MockWith {
112+
@((New-FakeFunction -Name 'Get-CIPPFoo'), (New-FakeFunction -Name 'Get-SomeOtherModuleThing'))
113+
}
114+
115+
$request = [pscustomobject]@{ Query = [pscustomobject]@{ Module = 'CIPPCore' } }
116+
$response = Invoke-Endpoint -Request $request
117+
118+
$response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK)
119+
$response.Body | Should -HaveCount 1
120+
$response.Body[0].Function | Should -Be 'Get-CIPPFoo'
121+
Should -Invoke Get-Help -Times 0 -Exactly
122+
}
123+
124+
It 'uses live Get-Help for everything when no cache file exists' {
125+
$env:CIPPRootPath = Join-Path $TestDrive 'empty'
126+
$null = New-Item -ItemType Directory -Path $env:CIPPRootPath -Force
127+
Mock -CommandName Get-Command -MockWith {
128+
@((New-FakeFunction -Name 'Get-CIPPFoo'), (New-FakeFunction -Name 'Get-CIPPBar'))
129+
}
130+
131+
$request = [pscustomobject]@{ Query = [pscustomobject]@{ Module = 'CIPPCore' } }
132+
$response = Invoke-Endpoint -Request $request
133+
134+
$response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK)
135+
$response.Body | Should -HaveCount 2
136+
Should -Invoke Get-Help -Times 2 -Exactly
137+
}
138+
139+
It 'filters out entrypoint functions listed in the cache' {
140+
$env:CIPPRootPath = Join-Path $TestDrive 'entrypoints'
141+
New-CacheFile -Root $env:CIPPRootPath -Functions @{
142+
'Get-CIPPFoo' = @{
143+
Functionality = ''
144+
Synopsis = 'cached synopsis'
145+
Parameters = @()
146+
}
147+
'Invoke-CIPPBar' = @{
148+
Functionality = 'Entrypoint,AnyTenant'
149+
Synopsis = 'an entrypoint'
150+
Parameters = @()
151+
}
152+
}
153+
Mock -CommandName Get-Command -MockWith {
154+
@((New-FakeFunction -Name 'Get-CIPPFoo'), (New-FakeFunction -Name 'Invoke-CIPPBar'))
155+
}
156+
157+
$request = [pscustomobject]@{ Query = [pscustomobject]@{ Module = 'CIPPCore' } }
158+
$response = Invoke-Endpoint -Request $request
159+
160+
$response.Body | Should -HaveCount 1
161+
$response.Body[0].Function | Should -Be 'Get-CIPPFoo'
162+
}
163+
}

0 commit comments

Comments
 (0)