From 49e4953f14e9e2bcddb41a859126624abe8c626d Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Tue, 22 Sep 2026 16:29:40 -0500 Subject: [PATCH 1/4] fix: Update-PiHoleActionsGravity had a completely dead formatted-output branch The non-RawOutput path built $ObjectFinal from $Object, but $Object was hardcoded to $null and never assigned, so every default (non-RawOutput) call silently returned an empty array regardless of whether gravity actually updated - there was no way to tell success from failure without RawOutput. The gravity endpoint streams back the pihole -g console log as plain text, not JSON, so there's no structured field to build a rich object from. The formatted output now returns {Status = "Completed"} to match the same pattern already used by Invoke-PiHoleFlushNetwork/Restart-PiHoleDnsService. Also: - Fixed a bug where -WhatIf (ShouldProcess declining) would still try to read $Response, which was never set in that case. - Removed the stray break in the catch block (inconsistent with every other function and would end the pipeline early on error). - Added SYNOPSIS/DESCRIPTION/PARAMETER/EXAMPLE help matching module convention, which also drops the README's WIP flag for this function. - Added an integration test covering the formatted output, verified against the real Pi-hole server (full gravity rebuild, ~80s runtime). Co-Authored-By: Claude Sonnet 5 --- .../Actions/Update-PiHoleActionsGravity.ps1 | 45 ++++++++++++------- README.md | 2 +- ...PiHoleActionsGravity.Integration.Tests.ps1 | 8 ++++ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/PiHoleShell/Public/Actions/Update-PiHoleActionsGravity.ps1 b/PiHoleShell/Public/Actions/Update-PiHoleActionsGravity.ps1 index 8d392db..3e23c04 100644 --- a/PiHoleShell/Public/Actions/Update-PiHoleActionsGravity.ps1 +++ b/PiHoleShell/Public/Actions/Update-PiHoleActionsGravity.ps1 @@ -1,10 +1,29 @@ function Update-PiHoleActionsGravity { <# .SYNOPSIS -Update Pi-hole's adlists by running pihole -g. The output of the process is streamed with chunked encoding. Use the optional color query parameter to include ANSI color escape codes in the output. +Update Pi-hole's adlists by running pihole -g +.DESCRIPTION +Triggers a Pi-hole gravity update (equivalent to running `pihole -g` on the server), which +re-downloads and rebuilds the adlists used for blocking. This can take anywhere from several +seconds to a few minutes depending on adlist size. The API streams back the raw `pihole -g` +console log as plain text rather than JSON, so -RawOutput returns that log verbatim. + +.PARAMETER PiHoleServer +The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" + +.PARAMETER Password +The API Password you generated from your PiHole server + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the raw pihole -g console log instead of the formatted object + +.EXAMPLE +Update-PiHoleActionsGravity -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> - #Work In Progress [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/gravity')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( @@ -26,27 +45,23 @@ Update Pi-hole's adlists by running pihole -g. The output of the process is stre ContentType = "application/json" } - if ($PSCmdlet.ShouldProcess("Pi-Hole server at $PiHoleServer", "Update gravity actions")) { + if ($PSCmdlet.ShouldProcess("Pi-Hole server at $PiHoleServer", "Update gravity")) { $Response = Invoke-RestMethod @Params - } - - if ($RawOutput) { - Write-Output $Response - } - else { - $ObjectFinal = @() - $Object = $null - if ($Object) { - $ObjectFinal += $Object + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = [PSCustomObject]@{ + Status = "Completed" + } + Write-Output $Object } - Write-Output $ObjectFinal } } catch { Write-Error -Message $_.Exception.Message - break } finally { diff --git a/README.md b/README.md index 31ddd4f..69693f0 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Functions marked 🚧 are still under active development — signatures and outp |---|---| | `Invoke-PiHoleFlushNetwork` | Flushes the network table. This includes removing both all known devices and their associated addresses. | | `Restart-PiHoleDnsService` | Restarts the pihole-FTL service | -| `Update-PiHoleActionsGravity` 🚧 | Update Pi-hole's adlists by running pihole -g. The output of the process is streamed with chunked encoding. Use the optional color query parameter to include ANSI color escape codes in the output. | +| `Update-PiHoleActionsGravity` | Update Pi-hole's adlists by running pihole -g | ### DNS Control diff --git a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 index a9a64bc..f14249b 100644 --- a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 +++ b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 @@ -26,6 +26,14 @@ Describe 'Update-PiHoleActionsGravity (Integration)' -Tag 'Integration' { } } + It 'runs a gravity update and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Update-PiHoleActionsGravity -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be "Completed" + } + It 'runs a gravity update and returns the raw API response' -Skip:(-not $script:ConfigAvailable) { $result = Update-PiHoleActionsGravity -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true -Confirm:$false Write-Host "RawOutput: [$result]" From 5c75b3b109cd4099eec24d1ae51eb12ae5f5f931 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Tue, 22 Sep 2026 16:43:15 -0500 Subject: [PATCH 2/4] test: print the captured error in the gravity bad-password test Matches the other two tests in this file, which already print the result they assert on for easier debugging when a run fails. Co-Authored-By: Claude Sonnet 5 --- tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 index f14249b..9257a43 100644 --- a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 +++ b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 @@ -43,6 +43,7 @@ Describe 'Update-PiHoleActionsGravity (Integration)' -Tag 'Integration' { It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { $result = Update-PiHoleActionsGravity -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error: [$errOut]" $errOut | Should -Not -BeNullOrEmpty } From aa6b7b2ba15e29677e512331f65118d68f7a22d0 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Tue, 22 Sep 2026 21:21:37 -0500 Subject: [PATCH 3/4] docs: add descriptions for Get-PiHoleDnsBlockingStatus and Set-PiHoleDnsBlocking Both functions had a raw Pi-hole API docs URL sitting in .SYNOPSIS instead of an actual synopsis, which is why the README's command reference table showed "No description yet" for them. Replaced with real SYNOPSIS text and added a .DESCRIPTION block for each, matching the convention used elsewhere in the module, and regenerated the README. Co-Authored-By: Claude Sonnet 5 --- .../Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 | 7 ++++++- PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 | 8 +++++++- README.md | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 b/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 index 804b3e7..f6fd6a6 100644 --- a/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 +++ b/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 @@ -1,7 +1,12 @@ function Get-PiHoleDnsBlockingStatus { <# .SYNOPSIS -https://ftl.pi-hole.net/master/docs/#get-/config +Get Pi-hole's current DNS blocking status + +.DESCRIPTION +Returns whether Pi-hole is currently blocking DNS queries. If blocking has been temporarily +toggled with a timer (see Set-PiHoleDnsBlocking), this also returns how many seconds remain +until the opposite setting reverts. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" diff --git a/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 b/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 index de140d2..8a7692b 100644 --- a/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 +++ b/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 @@ -1,7 +1,13 @@ function Set-PiHoleDnsBlocking { <# .SYNOPSIS -https://ftl.pi-hole.net/master/docs/#get-/dns/blocking +Enable or disable Pi-hole's DNS blocking + +.DESCRIPTION +Turns Pi-hole's DNS blocking on or off. Optionally pass -TimeInSeconds to have Pi-hole +automatically revert to the opposite setting after that many seconds - for example, +disabling blocking for 60 seconds to temporarily let all DNS queries through, after which +blocking resumes on its own. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" diff --git a/README.md b/README.md index 69693f0..844e867 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ Functions marked 🚧 are still under active development — signatures and outp | Function | Description | |---|---| -| `Get-PiHoleDnsBlockingStatus` | _No description yet_ | -| `Set-PiHoleDnsBlocking` | _No description yet_ | +| `Get-PiHoleDnsBlockingStatus` | Get Pi-hole's current DNS blocking status | +| `Set-PiHoleDnsBlocking` | Enable or disable Pi-hole's DNS blocking | ### Group Management From f876f3d9ae0bab843d54c138a5cea85910371027 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Tue, 22 Sep 2026 22:00:27 -0500 Subject: [PATCH 4/4] fix: make Add-PiHoleList/Remove-PiHoleList production ready, add integration tests Add-PiHoleList and Remove-PiHoleList were both marked Work In Progress and, per the live Pi-hole OpenAPI spec, functionally broken: - Add-PiHoleList sent `type` in the JSON body, but POST /api/lists requires it as a query parameter (?type=block|allow). Every call 400'd. - Add-PiHoleList's DateUpdated calculation was hoisted outside its own foreach loop, evaluating against an undefined $Item on every call. - Remove-PiHoleList's formatted output tried to build objects from $Response.lists, but a successful delete is HTTP 204 No Content - there's no body to parse, so it always silently returned an empty array. Replaced with a simple {Address, Type, Status} object, matching the pattern used by Invoke-PiHoleFlushNetwork/Restart-PiHoleDnsService/Update-PiHoleActionsGravity for actions with no meaningful response body. - Remove-PiHoleList had a dead pre-check (looked up the list, then did nothing with the result) and didn't mark Address/Type as mandatory. Also fixed a shared bug in Request-PiHoleAuth (the internal auth helper every function calls): its catch block had a stray `break`, and [CmdletBinding()] was misplaced inside param() instead of above it. The break meant any auth failure - even with -ErrorAction SilentlyContinue - would silently abort the entire calling script/pipeline, not just the function. This directly blocked writing correct "bad password" tests for Add-PiHoleList (which calls Get-PiHoleList and Get-PiHoleGroup internally to check for an existing list before adding), so the identical stray `break` was also removed from those two functions. Both are direct, unavoidable dependencies of Add-PiHoleList's own logic. Verified against a real Pi-hole v6 server: full add/remove round trip, duplicate-add rejection, RawOutput on both, and bad-password errors on both, using https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt as the test list. Added dedicated integration test files for both functions. README regenerated to drop the WIP flag now that both are functional. Co-Authored-By: Claude Sonnet 5 --- .../Authentication/Request-PiHoleAuth.ps1 | 3 +- .../GroupManagement/Get-PiHoleGroup.ps1 | 1 - .../Public/ListManagement/Add-PiHoleList.ps1 | 43 +++++++---- .../Public/ListManagement/Get-PiHoleList.ps1 | 1 - .../ListManagement/Remove-PiHoleList.ps1 | 58 ++++++--------- README.md | 4 +- tests/Add-PiHoleList.Integration.Tests.ps1 | 74 +++++++++++++++++++ tests/Remove-PiHoleList.Integration.Tests.ps1 | 67 +++++++++++++++++ 8 files changed, 198 insertions(+), 53 deletions(-) create mode 100644 tests/Add-PiHoleList.Integration.Tests.ps1 create mode 100644 tests/Remove-PiHoleList.Integration.Tests.ps1 diff --git a/PiHoleShell/Public/Authentication/Request-PiHoleAuth.ps1 b/PiHoleShell/Public/Authentication/Request-PiHoleAuth.ps1 index aaec1a4..6428a97 100644 --- a/PiHoleShell/Public/Authentication/Request-PiHoleAuth.ps1 +++ b/PiHoleShell/Public/Authentication/Request-PiHoleAuth.ps1 @@ -1,8 +1,8 @@ function Request-PiHoleAuth { #INTERNAL FUNCTION + [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( - [CmdletBinding()] [System.URI]$PiHoleServer, [string]$Password, [bool]$IgnoreSsl = $false @@ -25,6 +25,5 @@ function Request-PiHoleAuth { catch { Write-Error -Message $_.Exception.Message - break } } \ No newline at end of file diff --git a/PiHoleShell/Public/GroupManagement/Get-PiHoleGroup.ps1 b/PiHoleShell/Public/GroupManagement/Get-PiHoleGroup.ps1 index 5bd952c..c93bc09 100644 --- a/PiHoleShell/Public/GroupManagement/Get-PiHoleGroup.ps1 +++ b/PiHoleShell/Public/GroupManagement/Get-PiHoleGroup.ps1 @@ -73,7 +73,6 @@ Get groups catch { Write-Error -Message $_.Exception.Message - break } finally { diff --git a/PiHoleShell/Public/ListManagement/Add-PiHoleList.ps1 b/PiHoleShell/Public/ListManagement/Add-PiHoleList.ps1 index af8acc5..d1b4ad3 100644 --- a/PiHoleShell/Public/ListManagement/Add-PiHoleList.ps1 +++ b/PiHoleShell/Public/ListManagement/Add-PiHoleList.ps1 @@ -1,7 +1,11 @@ function Add-PiHoleList { <# .SYNOPSIS -Add new list +Add a new list + +.DESCRIPTION +Subscribes Pi-hole to a new allow or block list. The list is fetched and applied the next +time gravity runs (see Update-PiHoleActionsGravity). .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" @@ -12,11 +16,27 @@ The API Password you generated from your PiHole server .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation +.PARAMETER Address +The URL of the list to subscribe to + +.PARAMETER Type +Whether this is an Allow list or a Block list + +.PARAMETER Comment +An optional comment to store alongside the list + +.PARAMETER Group +The group(s) this list applies to. Defaults to "Default" + +.PARAMETER Enabled +Whether the list is enabled immediately. Defaults to $true + .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Add-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Address "https://hosts-file.net/ad_servers.txt" -Type Block #> - #Work In Progress [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/lists')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( @@ -25,6 +45,7 @@ This will dump the response instead of the formatted object [Parameter(Mandatory = $true)] [string]$Password, [bool]$IgnoreSsl = $false, + [Parameter(Mandatory = $true)] [System.Uri]$Address, [Parameter(Mandatory = $true)] [ValidateSet("Allow", "Block")] @@ -61,7 +82,6 @@ This will dump the response instead of the formatted object $Body = @{ address = $Address - type = $Type groups = [Object[]]($AllGroupsIds) comment = $Comment enabled = $Enabled @@ -69,7 +89,7 @@ This will dump the response instead of the formatted object $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/lists" + Uri = "$($PiHoleServer.OriginalString)/api/lists?type=$($Type.ToLower())" Method = "Post" SkipCertificateCheck = $IgnoreSsl Body = $Body | ConvertTo-Json -Depth 10 @@ -78,21 +98,19 @@ This will dump the response instead of the formatted object $Response = Invoke-RestMethod @Params - if ($Item.date_updated -eq 0) { - $DateUpdated = $null - } - else { - $DateUpdated = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime - } - if ($RawOutput) { Write-Output $Response } else { $ObjectFinal = @() - $Object = $null foreach ($Item in $Response.lists) { + if ($Item.date_updated -eq 0) { + $DateUpdated = $null + } + else { + $DateUpdated = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime + } $Object = [PSCustomObject]@{ Address = $Item.address @@ -120,7 +138,6 @@ This will dump the response instead of the formatted object catch { Write-Error -Message $_.Exception.Message - break } finally { diff --git a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 index 5539fda..50d09ba 100644 --- a/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 +++ b/PiHoleShell/Public/ListManagement/Get-PiHoleList.ps1 @@ -88,7 +88,6 @@ This will dump the response instead of the formatted object catch { Write-Error -Message $_.Exception.Message - break } finally { diff --git a/PiHoleShell/Public/ListManagement/Remove-PiHoleList.ps1 b/PiHoleShell/Public/ListManagement/Remove-PiHoleList.ps1 index 93e3d21..0fdc319 100644 --- a/PiHoleShell/Public/ListManagement/Remove-PiHoleList.ps1 +++ b/PiHoleShell/Public/ListManagement/Remove-PiHoleList.ps1 @@ -1,7 +1,11 @@ function Remove-PiHoleList { <# .SYNOPSIS -Deletes multiple lists in the lists object. +Remove a list + +.DESCRIPTION +Unsubscribes Pi-hole from an allow or block list. The Pi-hole API deletes lists in a batch, +so this sends a single-item batch containing just the list you specify. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" @@ -12,11 +16,18 @@ The API Password you generated from your PiHole server .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation +.PARAMETER Address +The URL of the list to remove + +.PARAMETER Type +Whether this is an Allow list or a Block list + .PARAMETER RawOutput -This will dump the response instead of the formatted objec +This will dump the response instead of the formatted object +.EXAMPLE +Remove-PiHoleList -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Address "https://hosts-file.net/ad_servers.txt" -Type Block #> - #Work In Progress (NEED TO FINISH) [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/lists-batchDelete')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( @@ -25,19 +36,16 @@ This will dump the response instead of the formatted objec [Parameter(Mandatory = $true)] [string]$Password, [bool]$IgnoreSsl = $false, + [Parameter(Mandatory = $true)] [System.Uri]$Address, + [Parameter(Mandatory = $true)] + [ValidateSet("Allow", "Block")] [string]$Type, [bool]$RawOutput = $false ) try { $Target = "Pi-Hole list $Address of type $Type" if ($PSCmdlet.ShouldProcess($Target, "Remove list")) { - $FindMatchingList = Get-PiHoleList -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl | Where-Object { $_.Address -eq $Address } - - if ($FindMatchingList) { - - } - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl $Body = @( @@ -65,38 +73,20 @@ This will dump the response instead of the formatted objec } else { - $ObjectFinal = @() - $Object = $null - - foreach ($Item in $Response.lists) { - $Object = [PSCustomObject]@{ - Address = $Item.address - Comment = $Item.comment - Groups = $AllGroupsNames - Enabled = $Item.enabled - Id = $Item.id - DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime - DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime - Type = $Item.type.SubString(0, 1).ToUpper() + $Item.type.SubString(1).ToLower() - DateUpdated = $DateUpdated - Number = $Item.number - InvalidDomains = $Item.invalid_domains - AbpEntries = $Item.abp_entries - Status = $Item.status - } - if ($Object) { - $ObjectFinal += $Object - } + # A successful delete returns 204 No Content, so there's no response body to + # build a rich object from. + $Object = [PSCustomObject]@{ + Address = $Address + Type = $Type + Status = "Removed" } - - Write-Output $ObjectFinal + Write-Output $Object } } } catch { Write-Error -Message $_.Exception.Message - break } finally { diff --git a/README.md b/README.md index 69693f0..0bdaf29 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ Functions marked 🚧 are still under active development — signatures and outp | Function | Description | |---|---| -| `Add-PiHoleList` 🚧 | Add new list | +| `Add-PiHoleList` | Add a new list | | `Get-PiHoleList` 🚧 | Get lists | -| `Remove-PiHoleList` 🚧 | Deletes multiple lists in the lists object. | +| `Remove-PiHoleList` | Remove a list | | `Search-PiHoleListDomain` | _No description yet_ | ### Metrics diff --git a/tests/Add-PiHoleList.Integration.Tests.ps1 b/tests/Add-PiHoleList.Integration.Tests.ps1 new file mode 100644 index 0000000..ce60d01 --- /dev/null +++ b/tests/Add-PiHoleList.Integration.Tests.ps1 @@ -0,0 +1,74 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Add-PiHoleList (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestListAddress = 'https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt' + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test list behind + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + # Ensures the test list is never left behind for other test files to trip over + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'adds a new list and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'Pester integration test list' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Address | Should -Be $script:TestListAddress + $result.Type | Should -Be 'Block' + $result.Enabled | Should -BeTrue + + # Clean up immediately so the next test starts from a known (list-absent) state + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.lists[0].address | Should -Be $script:TestListAddress + + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null + } + + It 'errors when the list already exists' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block | Out-Null + + $result = Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error: [$errOut]" + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false | Out-Null + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error: [$errOut]" + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Remove-PiHoleList.Integration.Tests.ps1 b/tests/Remove-PiHoleList.Integration.Tests.ps1 new file mode 100644 index 0000000..ff9df8e --- /dev/null +++ b/tests/Remove-PiHoleList.Integration.Tests.ps1 @@ -0,0 +1,67 @@ +# Requires -Module Pester +# +# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1 +# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically +# if that file is missing. + +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleList (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestListAddress = 'https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt' + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Defensive cleanup in case a previous failed run left the test list behind + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + # Defensive cleanup in case a test left the list behind + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'removes an existing list and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Comment 'Pester integration test list' | Out-Null + + $result = Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Address | Should -Be $script:TestListAddress + $result.Status | Should -Be 'Removed' + + $remaining = Get-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl | Where-Object { $_.Address -eq $script:TestListAddress } + $remaining | Should -BeNullOrEmpty + } + + It 'removes the list when RawOutput is set, even though the API returns no body' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block | Out-Null + + # A successful delete is HTTP 204 No Content, so RawOutput is expected to be empty here - + # the list actually being gone afterward is the real signal of success. + Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -RawOutput $true -Confirm:$false + + $remaining = Get-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl | Where-Object { $_.Address -eq $script:TestListAddress } + $remaining | Should -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block | Out-Null + + $result = Remove-PiHoleList -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Address $script:TestListAddress -Type Block -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error: [$errOut]" + + $errOut | Should -Not -BeNullOrEmpty + } +}