From 684faa57c18f091537d1e2e5f412d10299d192fb Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 17 Sep 2026 12:32:11 -0500 Subject: [PATCH 1/6] fix: correct flush-network endpoint and double-slash URL bug Invoke-PiHoleFlushNetwork posted to /api/action/flush/logs instead of /api/action/flush/network. Both it and Restart-PiHoleDnsService built the request URL via string interpolation of the [uri] PiHoleServer value, which appends a trailing slash and produced a double slash that the real API 404s on. Both now use .ToString().TrimEnd('/'). Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 | 2 +- PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 index 87c0847..679ac45 100644 --- a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 +++ b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 @@ -36,7 +36,7 @@ Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Passwor $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$PiHoleServer/api/action/flush/logs" + Uri = "$($PiHoleServer.ToString().TrimEnd('/'))/api/action/flush/network" Method = "Post" ContentType = "application/json" SkipCertificateCheck = $IgnoreSsl diff --git a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 index 7ee1e4a..fc3c6fa 100644 --- a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 +++ b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 @@ -36,7 +36,7 @@ Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$PiHoleServer/api/action/restartdns" + Uri = "$($PiHoleServer.ToString().TrimEnd('/'))/api/action/restartdns" Method = "Post" ContentType = "application/json" SkipCertificateCheck = $IgnoreSsl From c0a2ad9f3ccd0417b646089b17bc011f271e217d Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 17 Sep 2026 12:32:24 -0500 Subject: [PATCH 2/6] test: add real-server integration tests for Actions functions Adds Pester integration tests for Invoke-PiHoleFlushNetwork, Restart-PiHoleDnsService, and Update-PiHoleActionsGravity that run against a live Pi-hole server instead of mocks. Tests are tagged 'Integration' and skip automatically unless tests/IntegrationConfig.local.ps1 is present; that file is gitignored so real server credentials never get committed. A tracked IntegrationConfig.example.ps1 documents the expected shape. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 +- tests/IntegrationConfig.example.ps1 | 9 ++++ ...e-PiHoleFlushNetwork.Integration.Tests.ps1 | 43 +++++++++++++++++ ...art-PiHoleDnsService.Integration.Tests.ps1 | 46 +++++++++++++++++++ ...PiHoleActionsGravity.Integration.Tests.ps1 | 40 ++++++++++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 tests/IntegrationConfig.example.ps1 create mode 100644 tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 create mode 100644 tests/Restart-PiHoleDnsService.Integration.Tests.ps1 create mode 100644 tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 diff --git a/.gitignore b/.gitignore index d669de9..e426d10 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -notes.txt \ No newline at end of file +notes.txt +tests/IntegrationConfig.local.ps1 \ No newline at end of file diff --git a/tests/IntegrationConfig.example.ps1 b/tests/IntegrationConfig.example.ps1 new file mode 100644 index 0000000..936e1fe --- /dev/null +++ b/tests/IntegrationConfig.example.ps1 @@ -0,0 +1,9 @@ +# Copy this file to 'IntegrationConfig.local.ps1' (same folder) and fill in your own +# server details. IntegrationConfig.local.ps1 is gitignored so your token is never committed. +# +# These values are consumed by the *.Integration.Tests.ps1 files, which make real calls +# against a live Pi-hole server. + +$PiHoleServer = [uri]'https://pihole.example.com:8489' +$PiHoleToken = 'your-api-token-here' +$PiHoleIgnoreSsl = $true diff --git a/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 b/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 new file mode 100644 index 0000000..16ff84e --- /dev/null +++ b/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 @@ -0,0 +1,43 @@ +# 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. + +# Config availability must be known at discovery time so the -Skip parameter on each It block +# (evaluated during discovery, before BeforeAll runs) sees the correct value. +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Invoke-PiHoleFlushNetwork (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + # Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because + # Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value. + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'flushes the network table and returns a formatted status' -Skip:(-not $script:ConfigAvailable) { + $result = Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Flushed' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + { Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true } | + Should -Not -Throw + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 b/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 new file mode 100644 index 0000000..abeaad9 --- /dev/null +++ b/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 @@ -0,0 +1,46 @@ +# 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. +# +# NOTE: this actually restarts the pihole-FTL service on the target server, causing a brief +# DNS resolution interruption on that server. + +# Config availability must be known at discovery time so the -Skip parameter on each It block +# (evaluated during discovery, before BeforeAll runs) sees the correct value. +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Restart-PiHoleDnsService (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + # Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because + # Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value. + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'restarts the DNS service and returns a formatted status' -Skip:(-not $script:ConfigAvailable) { + $result = Restart-PiHoleDnsService -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Restarted' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + { Restart-PiHoleDnsService -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true } | + Should -Not -Throw + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Restart-PiHoleDnsService -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 new file mode 100644 index 0000000..35a9fc9 --- /dev/null +++ b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 @@ -0,0 +1,40 @@ +# 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. +# +# NOTE: this actually runs `pihole -g` on the target server (rebuilds the gravity/adlists), +# which can take anywhere from several seconds to a few minutes depending on adlist size. + +# Config availability must be known at discovery time so the -Skip parameter on each It block +# (evaluated during discovery, before BeforeAll runs) sees the correct value. +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Update-PiHoleActionsGravity (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + # Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because + # Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value. + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + 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 + + $result | Should -Not -BeNullOrEmpty + } + + 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 + + $errOut | Should -Not -BeNullOrEmpty + } +} From 6b24e16b78338dcabdc565e58d275ac3e1b686c8 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 17 Sep 2026 12:32:30 -0500 Subject: [PATCH 3/6] docs: modernize README Add badges, a table of contents, a command reference grouped by category (flagging work-in-progress functions), and a testing section covering the new unit/integration Pester split. Also fixes the Pi-hole app-password screenshot links, which used backslash paths that don't render on GitHub. Co-Authored-By: Claude Sonnet 5 --- README.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 155 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 8104f20..da2c77e 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,170 @@ # PiHoleShell -A PowerShell module for PiHole v6 API. + +[![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/PiHoleShell?label=PowerShell%20Gallery)](https://www.powershellgallery.com/packages/PiHoleShell) +[![PowerShell Gallery Downloads](https://img.shields.io/powershellgallery/dt/PiHoleShell)](https://www.powershellgallery.com/packages/PiHoleShell) +[![CI](https://github.com/mikemadeja/PiHoleShell/actions/workflows/PSScriptAnalyzer.yml/badge.svg)](https://github.com/mikemadeja/PiHoleShell/actions/workflows/PSScriptAnalyzer.yml) +[![License: Apache 2.0](https://img.shields.io/github/license/mikemadeja/PiHoleShell)](LICENSE) +![PowerShell 7+](https://img.shields.io/badge/PowerShell-7%2B%20(Core)-blue) + +A PowerShell module for automating and scripting against the **Pi-hole v6 REST API** — DNS blocking control, allow/deny lists, groups, stats, and server actions, all from PowerShell. + +> This module targets Pi-hole's v6 API only. It will not work against Pi-hole v5 or earlier. + +## Table of Contents + +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [Getting an API Password](#getting-an-api-password) +- [Quick Start](#quick-start) +- [Command Reference](#command-reference) +- [Testing](#testing) +- [Contributing](#contributing) +- [License](#license) + +## Features + +- Enable/disable DNS blocking, optionally for a set duration +- Manage allow/deny lists and groups +- Trigger server actions: flush network table, restart DNS, update gravity +- Pull stats, summaries, and diagnostic info +- Every function authenticates and closes its own session automatically — no manual login/logout calls needed + +## Requirements + +- **PowerShell 7.0+ (Core edition)** — the module refuses to load on Windows PowerShell 5.1 or other editions +- A reachable Pi-hole v6 server and an API app password ## Installation -It is recommended to install this from https://www.powershellgallery.com/packages/PiHoleShell +Install from the [PowerShell Gallery](https://www.powershellgallery.com/packages/PiHoleShell): + +```powershell +Install-Module -Name PiHoleShell -Scope CurrentUser +Import-Module -Name PiHoleShell +``` -## Contributions +## Getting an API Password -I am in the beginning stages of developing this. I am open to suggestions or contributors and am learning as I go. I hope this will bring some value to people :-). +1. Log into your Pi-hole web interface, then go to **Web Interface / API** settings and select **Configure app password**. -## How to use + Pi-hole Web Interface / API settings -Generate an app password by logging into your PiHole server. +2. Copy the generated password, then click **Enable new app password**. -Click Web Interface / Api + Configure app password dialog -Click Configure app password +Keep this password secret — anyone with it has full API access to your Pi-hole. -drawing +## Quick Start -Copy your password, then click Enable new app password. +```powershell +$PiHoleServer = "https://pihole.example.com:8489" +$Password = "" -drawing +# Check whether blocking is currently enabled +Get-PiHoleDnsBlockingStatus -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl:$true +# Disable blocking for 5 minutes, then it re-enables automatically +Set-PiHoleDnsBlocking -PiHoleServer $PiHoleServer -Password $Password -Blocking False -TimeInSeconds 300 -IgnoreSsl:$true ``` -PS Install-Module -Name PiHoleShell -PS Import-Module -Name PiHoleShell -PS Get-PiHoleDnsBlockingStatus -PiHoleServer http://PIHOLESERVER.DOMAIN.COM -Password "APPPASSWORD" -IgnoreSsl:$true - -Blocking Timer --------- ----- -enabled 0 -``` \ No newline at end of file + +Every function accepts the same core parameters: + +| Parameter | Description | +|---|---| +| `-PiHoleServer` | Base URL of your Pi-hole, e.g. `https://pihole.example.com:8489` | +| `-Password` | The app password from [Getting an API Password](#getting-an-api-password) | +| `-IgnoreSsl` | Skip TLS certificate validation (useful for self-signed certs) | +| `-RawOutput` | Return the unmodified API response instead of a formatted object | + +## Command Reference + +Functions marked 🚧 are still under active development — signatures and output shapes may change. + +### Actions + +| Function | Description | +|---|---| +| `Invoke-PiHoleFlushNetwork` | Flush the network table, removing known devices and their addresses | +| `Restart-PiHoleDnsService` | Restart the `pihole-FTL` service | +| `Update-PiHoleActionsGravity` 🚧 | Run `pihole -g` to rebuild the gravity/adlists database | + +### DNS Control + +| Function | Description | +|---|---| +| `Get-PiHoleDnsBlockingStatus` | Get current blocking status and any active timer | +| `Set-PiHoleDnsBlocking` | Enable or disable blocking, optionally for a set duration | + +### Group Management + +| Function | Description | +|---|---| +| `Get-PiHoleGroup` | List groups | +| `New-PiHoleGroup` | Create a group | +| `Update-PiHoleGroup` | Update an existing group | +| `Remove-PiHoleGroup` 🚧 | Delete a group | + +### List Management + +| Function | Description | +|---|---| +| `Get-PiHoleList` 🚧 | List allow/deny lists | +| `Add-PiHoleList` 🚧 | Add a domain to an allow/deny list | +| `Remove-PiHoleList` 🚧 | Remove lists | +| `Search-PiHoleListDomain` | Search all lists for a domain, with optional partial matching | + +### Metrics + +| Function | Description | +|---|---| +| `Get-PiHoleStatsSummary` | Overview of query, system, and FTL activity | +| `Get-PiHoleStatsRecentBlocked` | Most recently blocked domain | +| `Get-PiHoleStatsQueryType` | Query breakdown by DNS record type | +| `Get-PiHoleStatsTopDomain` | Top permitted/blocked domains | +| `Get-PiHoleStatsTopClient` | Top clients by query volume | + +### Configuration & Diagnostics + +| Function | Description | +|---|---| +| `Get-PiHoleConfig` 🚧 | Read the Pi-hole configuration | +| `Get-PiHolePadd` 🚧 | Data used to power the PADD dashboard | +| `Get-PiHoleInfoMessage` | Pi-hole diagnosis messages | +| `Get-PiHoleInfoHost` 🚧 | Host system information | + +### Authentication + +Session handling is automatic for every command above, but these are available for managing sessions directly: + +| Function | Description | +|---|---| +| `Get-PiHoleCurrentAuthSession` | List active API sessions | +| `Remove-PiHoleAuthSession` | Revoke a session by ID | + +## Testing + +The module ships with two kinds of [Pester](https://pester.dev/) tests under `tests/`: + +- **Unit tests** (`*.Tests.ps1`) mock the API and run anywhere: + + ```powershell + Invoke-Pester -Path .\tests -ExcludeTagFilter Integration + ``` + +- **Integration tests** (`*.Integration.Tests.ps1`) run against a real Pi-hole server and are skipped automatically unless configured. To run them, copy `tests/IntegrationConfig.example.ps1` to `tests/IntegrationConfig.local.ps1` (gitignored) and fill in your server URL and app password, then run: + + ```powershell + Invoke-Pester -Path .\tests -TagFilter Integration + ``` + + These make real changes on the target server (they flush the network table, restart DNS, and rebuild gravity) — point them at a test instance, not production, if you'd rather not disrupt it. + +## Contributing + +This project is still early and growing. Issues, suggestions, and pull requests are welcome — see the 🚧 items in the [Command Reference](#command-reference) above for functions that could use testing or polish. + +## License + +[Apache License 2.0](LICENSE) From 6f58c74c26b42090049a66226aa612d43daa9e79 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 17 Sep 2026 17:41:19 -0500 Subject: [PATCH 4/6] ci: add Azure DevOps pipeline to run Pester tests on-prem Runs on the mmadeja-dt self-hosted agent so it can reach the internal Pi-hole server and exercise both the mocked unit tests and the real-server integration tests. The integration config file is written from secret pipeline variables (PiHoleTestServer/PiHoleTestToken) and deleted after the run regardless of outcome. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 +- azuredevops-pihole-pester-tests.yml | 69 +++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 azuredevops-pihole-pester-tests.yml diff --git a/.gitignore b/.gitignore index e426d10..78f21ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ notes.txt -tests/IntegrationConfig.local.ps1 \ No newline at end of file +tests/IntegrationConfig.local.ps1 +TestResults/ \ No newline at end of file diff --git a/azuredevops-pihole-pester-tests.yml b/azuredevops-pihole-pester-tests.yml new file mode 100644 index 0000000..8472474 --- /dev/null +++ b/azuredevops-pihole-pester-tests.yml @@ -0,0 +1,69 @@ +trigger: + branches: + include: + - main + - develop + +pr: + branches: + include: + - main + - develop + +pool: + name: mmadeja-dt + +variables: + testResultsFile: '$(System.DefaultWorkingDirectory)/TestResults/pester.xml' + +steps: + - pwsh: | + $pester = Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge [version]'5.0.0' } | Select-Object -First 1 + if (-not $pester) { + Write-Host "Installing Pester 5..." + Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck -Scope CurrentUser + } + else { + Write-Host "Using Pester $($pester.Version)" + } + displayName: 'Ensure Pester 5 is available' + + - pwsh: | + $configContent = @" + `$PiHoleServer = [uri]'$env:PIHOLE_TEST_SERVER' + `$PiHoleToken = '$env:PIHOLE_TEST_TOKEN' + `$PiHoleIgnoreSsl = `$true + "@ + Set-Content -Path (Join-Path '$(System.DefaultWorkingDirectory)' 'tests/IntegrationConfig.local.ps1') -Value $configContent -Encoding utf8 + displayName: 'Write integration test config' + env: + PIHOLE_TEST_SERVER: $(PiHoleTestServer) + PIHOLE_TEST_TOKEN: $(PiHoleTestToken) + + - pwsh: | + Import-Module Pester -MinimumVersion 5.0.0 -Force + + $config = New-PesterConfiguration + $config.Run.Path = './tests' + $config.Run.Exit = $true + $config.Output.Verbosity = 'Detailed' + $config.TestResult.Enabled = $true + $config.TestResult.OutputFormat = 'NUnitXml' + $config.TestResult.OutputPath = '$(testResultsFile)' + + Invoke-Pester -Configuration $config + displayName: 'Run Pester tests' + workingDirectory: '$(System.DefaultWorkingDirectory)' + + - pwsh: | + Remove-Item -Path (Join-Path '$(System.DefaultWorkingDirectory)' 'tests/IntegrationConfig.local.ps1') -Force -ErrorAction SilentlyContinue + displayName: 'Clean up integration test config' + condition: always() + + - task: PublishTestResults@2 + displayName: 'Publish test results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(testResultsFile)' + failTaskOnFailedTests: false From 8f1f7343766fc023e42bc18eaf251e8f486385ea Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 17 Sep 2026 17:56:28 -0500 Subject: [PATCH 5/6] fix: type PiHoleServer as [System.URI] and use OriginalString for URL building Matches the pattern already used elsewhere in the module. Building the request URL from .OriginalString instead of ToString().TrimEnd('/') needs PiHoleServer to actually be a [uri] object; it was previously untyped, so a caller passing a plain string (as the function's own .EXAMPLE showed) would have silently produced a broken relative URL. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 | 5 +++-- PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 index 679ac45..e08086a 100644 --- a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 +++ b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 @@ -25,7 +25,8 @@ Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Passwor [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Flushes PiHole logs')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( - $PiHoleServer, + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, $Password, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false @@ -36,7 +37,7 @@ Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Passwor $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.ToString().TrimEnd('/'))/api/action/flush/network" + Uri = "$($PiHoleServer.OriginalString)/api/action/flush/network" Method = "Post" ContentType = "application/json" SkipCertificateCheck = $IgnoreSsl diff --git a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 index fc3c6fa..6a8f3dd 100644 --- a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 +++ b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 @@ -25,7 +25,8 @@ Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Restarts PiHole DNS')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( - $PiHoleServer, + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, $Password, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false @@ -36,7 +37,7 @@ Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.ToString().TrimEnd('/'))/api/action/restartdns" + Uri = "$($PiHoleServer.OriginalString)/api/action/restartdns" Method = "Post" ContentType = "application/json" SkipCertificateCheck = $IgnoreSsl From 58098a60ae696c47d743bac8f48af4e583d97311 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Thu, 17 Sep 2026 18:05:27 -0500 Subject: [PATCH 6/6] fix: make Password mandatory and typed on Actions functions Invoke-PiHoleFlushNetwork and Restart-PiHoleDnsService were the only functions in the module missing [Parameter(Mandatory = $true)] [string]$Password, unlike every other public function. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 | 3 ++- PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 index e08086a..e1b6a8c 100644 --- a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 +++ b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 @@ -27,7 +27,8 @@ Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Passwor param ( [Parameter(Mandatory = $true)] [System.URI]$PiHoleServer, - $Password, + [Parameter(Mandatory = $true)] + [string]$Password, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) diff --git a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 index 6a8f3dd..2257393 100644 --- a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 +++ b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 @@ -27,7 +27,8 @@ Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password param ( [Parameter(Mandatory = $true)] [System.URI]$PiHoleServer, - $Password, + [Parameter(Mandatory = $true)] + [string]$Password, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false )