From 34bfebf94ca2ad33e02ae4167dea8eb57c737c1e Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 17:01:15 -0500 Subject: [PATCH 01/15] test: print returned objects in integration tests Adds Write-Host output (Format-List/Format-Table for structured results, direct interpolation for RawOutput) to every integration test so the actual data returned by the real server is visible in Pester's console output (with -Output Detailed), not just pass/fail. Also dropped two now-redundant Should -Not -Throw wrappers where the result is captured directly - an unhandled exception during that capture already fails the test. Co-Authored-By: Claude Sonnet 5 --- tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 | 2 ++ tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 | 2 ++ tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 | 5 +++-- tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 | 5 +++-- tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 | 3 +++ tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 | 2 ++ tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 | 3 +++ tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 | 5 +++-- tests/Restart-PiHoleDnsService.Integration.Tests.ps1 | 5 +++-- tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 | 1 + 10 files changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 index fe70c55..dbf8f27 100644 --- a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 @@ -25,6 +25,7 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { It 'returns a formatted object with all query type properties' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty $result.PSObject.Properties.Name | Should -Contain 'A' @@ -33,6 +34,7 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.PSObject.Properties.Name | Should -Contain 'types' } diff --git a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 index 8f2dbdd..a715f44 100644 --- a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 @@ -25,6 +25,7 @@ Describe 'Get-PiHoleStatsDatabaseSummary (Integration)' -Tag 'Integration' { It 'returns database summary as a formatted object' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseSummary -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty $result.SumQueries | Should -BeGreaterOrEqual 0 @@ -33,6 +34,7 @@ Describe 'Get-PiHoleStatsDatabaseSummary (Integration)' -Tag 'Integration' { It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseSummary -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.PSObject.Properties.Name | Should -Contain 'sum_queries' } diff --git a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 index f62aa5c..00b75ed 100644 --- a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 @@ -24,12 +24,13 @@ Describe 'Get-PiHoleStatsDatabaseTopClient (Integration)' -Tag 'Integration' { } It 'returns top clients without error' -Skip:(-not $script:ConfigAvailable) { - { Get-PiHoleStatsDatabaseTopClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 } | - Should -Not -Throw + $result = Get-PiHoleStatsDatabaseTopClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host } It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseTopClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.PSObject.Properties.Name | Should -Contain 'clients' } diff --git a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 index 0821b53..4d7ce6a 100644 --- a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 @@ -24,12 +24,13 @@ Describe 'Get-PiHoleStatsDatabaseTopDomain (Integration)' -Tag 'Integration' { } It 'returns top domains without error' -Skip:(-not $script:ConfigAvailable) { - { Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 } | - Should -Not -Throw + $result = Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host } It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.PSObject.Properties.Name | Should -Contain 'domains' } diff --git a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 index 5130c86..e6d7011 100644 --- a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 @@ -25,6 +25,8 @@ Describe 'Get-PiHoleStatsDatabaseUpstream (Integration)' -Tag 'Integration' { It 'returns upstream metrics as a formatted object' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseUpstream -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + $result.Upstreams | Format-Table | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty $result.TotalQueries | Should -BeGreaterOrEqual 0 @@ -32,6 +34,7 @@ Describe 'Get-PiHoleStatsDatabaseUpstream (Integration)' -Tag 'Integration' { It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseUpstream -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.total_queries | Should -Not -BeNullOrEmpty } diff --git a/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 b/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 index d74623a..b420af7 100644 --- a/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 @@ -25,6 +25,7 @@ Describe 'Get-PiHoleStatsQuerySuggestions (Integration)' -Tag 'Integration' { It 'returns filter suggestions as a formatted object' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsQuerySuggestions -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty $result.Type | Should -Not -BeNullOrEmpty @@ -35,6 +36,7 @@ Describe 'Get-PiHoleStatsQuerySuggestions (Integration)' -Tag 'Integration' { It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsQuerySuggestions -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.suggestions | Should -Not -BeNullOrEmpty } diff --git a/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 b/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 index 1f501c5..ec6250e 100644 --- a/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 @@ -21,6 +21,8 @@ Describe 'Get-PiHoleStatsUpstream (Integration)' -Tag 'Integration' { It 'returns upstream metrics as a formatted object' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsUpstream -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + $result.Upstreams | Format-Table | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty $result.Upstreams | Should -Not -BeNullOrEmpty @@ -29,6 +31,7 @@ Describe 'Get-PiHoleStatsUpstream (Integration)' -Tag 'Integration' { It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsUpstream -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host $result.upstreams | Should -Not -BeNullOrEmpty } diff --git a/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 b/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 index 16ff84e..c461e9a 100644 --- a/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 +++ b/tests/Invoke-PiHoleFlushNetwork.Integration.Tests.ps1 @@ -25,14 +25,15 @@ Describe 'Invoke-PiHoleFlushNetwork (Integration)' -Tag 'Integration' { 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 | Format-List | Out-String | Write-Host $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 + $result = Invoke-PiHoleFlushNetwork -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + Write-Host "RawOutput: [$result]" } It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { diff --git a/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 b/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 index 31da77c..90f4a3f 100644 --- a/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 +++ b/tests/Restart-PiHoleDnsService.Integration.Tests.ps1 @@ -28,6 +28,7 @@ Describe 'Restart-PiHoleDnsService (Integration)' -Tag 'Integration' { 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 | Format-List | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty $result.Status | Should -Be 'Restarted' @@ -38,8 +39,8 @@ Describe 'Restart-PiHoleDnsService (Integration)' -Tag 'Integration' { # restarting it again, or this occasionally hits a transient connection failure. Start-Sleep -Seconds 5 - { Restart-PiHoleDnsService -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true } | - Should -Not -Throw + $result = Restart-PiHoleDnsService -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + Write-Host "RawOutput: [$result]" } It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { diff --git a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 index 35a9fc9..a9a64bc 100644 --- a/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 +++ b/tests/Update-PiHoleActionsGravity.Integration.Tests.ps1 @@ -28,6 +28,7 @@ Describe 'Update-PiHoleActionsGravity (Integration)' -Tag 'Integration' { 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]" $result | Should -Not -BeNullOrEmpty } From ce3e0a58e01f0ab02c029f1ca6526987e6154e09 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 17:33:54 -0500 Subject: [PATCH 02/15] test: seed real DNS query traffic before stats integration tests Adds Initialize-PiHoleTestData.ps1, which resolves a random mix of well-known domains and known ad/tracker domains directly against the Pi-hole's DNS resolver, so its query stats aren't all zero/empty. Wired into the BeforeAll of every stats-related integration test (live and database-backed). Live stats reflect the seeded queries immediately. The on-disk database stats only reflect them once FTL's periodic flush to the long-term database runs, so this mainly builds up real history across repeated runs rather than guaranteeing non-zero results within the same run - confirmed empirically before landing on this design. Co-Authored-By: Claude Sonnet 5 --- ...atsDatabaseQueryType.Integration.Tests.ps1 | 5 ++ ...StatsDatabaseSummary.Integration.Tests.ps1 | 5 ++ ...atsDatabaseTopClient.Integration.Tests.ps1 | 5 ++ ...atsDatabaseTopDomain.Integration.Tests.ps1 | 5 ++ ...tatsDatabaseUpstream.Integration.Tests.ps1 | 5 ++ ...tatsQuerySuggestions.Integration.Tests.ps1 | 3 ++ ...-PiHoleStatsUpstream.Integration.Tests.ps1 | 3 ++ tests/Initialize-PiHoleTestData.ps1 | 52 +++++++++++++++++++ 8 files changed, 83 insertions(+) create mode 100644 tests/Initialize-PiHoleTestData.ps1 diff --git a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 index dbf8f27..76f9356 100644 --- a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 @@ -16,6 +16,11 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic. Live stats reflect it immediately; the on-disk + # database stats this file tests only reflect it once FTL's periodic flush runs, so + # this mainly helps build up real history across repeated runs, not this run's own data. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. diff --git a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 index a715f44..cca6731 100644 --- a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 @@ -16,6 +16,11 @@ Describe 'Get-PiHoleStatsDatabaseSummary (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic. Live stats reflect it immediately; the on-disk + # database stats this file tests only reflect it once FTL's periodic flush runs, so + # this mainly helps build up real history across repeated runs, not this run's own data. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. diff --git a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 index 00b75ed..abbfccf 100644 --- a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 @@ -16,6 +16,11 @@ Describe 'Get-PiHoleStatsDatabaseTopClient (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic. Live stats reflect it immediately; the on-disk + # database stats this file tests only reflect it once FTL's periodic flush runs, so + # this mainly helps build up real history across repeated runs, not this run's own data. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. diff --git a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 index 4d7ce6a..2d5a637 100644 --- a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 @@ -16,6 +16,11 @@ Describe 'Get-PiHoleStatsDatabaseTopDomain (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic. Live stats reflect it immediately; the on-disk + # database stats this file tests only reflect it once FTL's periodic flush runs, so + # this mainly helps build up real history across repeated runs, not this run's own data. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. diff --git a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 index e6d7011..a99fbc3 100644 --- a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 @@ -16,6 +16,11 @@ Describe 'Get-PiHoleStatsDatabaseUpstream (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic. Live stats reflect it immediately; the on-disk + # database stats this file tests only reflect it once FTL's periodic flush runs, so + # this mainly helps build up real history across repeated runs, not this run's own data. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. diff --git a/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 b/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 index b420af7..7ae2124 100644 --- a/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsQuerySuggestions.Integration.Tests.ps1 @@ -20,6 +20,9 @@ Describe 'Get-PiHoleStatsQuerySuggestions (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } } diff --git a/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 b/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 index ec6250e..5b415be 100644 --- a/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsUpstream.Integration.Tests.ps1 @@ -16,6 +16,9 @@ Describe 'Get-PiHoleStatsUpstream (Integration)' -Tag 'Integration' { $script:PiHoleServer = $PiHoleServer $script:PiHoleToken = $PiHoleToken $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } } diff --git a/tests/Initialize-PiHoleTestData.ps1 b/tests/Initialize-PiHoleTestData.ps1 new file mode 100644 index 0000000..d593a1f --- /dev/null +++ b/tests/Initialize-PiHoleTestData.ps1 @@ -0,0 +1,52 @@ +<# +.SYNOPSIS +Generates some real DNS query traffic against a Pi-hole server so its stats aren't all zero. + +.DESCRIPTION +Resolves a random mix of well-known domains (for forwarded/cached queries) and known +ad/tracker domains (for blocked queries) directly against the Pi-hole's DNS resolver, using +the same hostname as the API server. Live stats (Get-PiHoleStats*) reflect this immediately; +the on-disk "database" stats (Get-PiHoleStatsDatabase*) only reflect it once FTL's periodic +flush to the long-term database runs, so don't expect seeded data to appear there within the +same test run - this is still useful for building up real history across repeated runs. + +Windows-only: relies on the Resolve-DnsName cmdlet. + +.PARAMETER DnsServer +Hostname or IP of the Pi-hole's DNS resolver (typically the same host as the API, without the +scheme or port - e.g. $PiHoleServer.Host). + +.PARAMETER Count +How many domains to query this run. +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [string]$DnsServer, + [int]$Count = 8 +) + +$permittedDomains = @( + 'example.com', 'wikipedia.org', 'github.com', 'microsoft.com', 'apple.com', + 'cloudflare.com', 'stackoverflow.com', 'reddit.com', 'nytimes.com', 'bbc.com', + 'mozilla.org', 'python.org' +) +$blockedDomains = @( + 'doubleclick.net', 'googlesyndication.com', 'googleadservices.com', + 'adservice.google.com', 'ads.pubmatic.com', 'analytics.google.com' +) + +$pool = $permittedDomains + $blockedDomains +$selected = $pool | Get-Random -Count ([Math]::Min($Count, $pool.Count)) + +foreach ($domain in $selected) { + try { + Resolve-DnsName -Name $domain -Server $DnsServer -ErrorAction Stop | Out-Null + Write-Verbose "Queried $domain" + } + catch { + # A blocked or non-existent domain can still throw here depending on how it's blocked; + # either way, the query itself already reached Pi-hole and was logged. + Write-Verbose "Query for $domain errored (likely blocked): $($_.Exception.Message)" + } +} From b4b368844c3f0912f3d347d0aaebd613757fd95f Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 17:34:03 -0500 Subject: [PATCH 03/15] fix: Get-PiHoleStatsTopDomain silently returned nothing by default It only had a RawOutput branch and no else, so with RawOutput $false (the default) the function produced no output at all - not wrong data, no data. Rewrote it to match its sibling Get-PiHoleStatsTopClient.ps1: a proper try/catch/finally with session cleanup and a formatted Domain/Count object array in the default case. Verified against a real server - now returns real top-domain data instead of nothing. Co-Authored-By: Claude Sonnet 5 --- .../Metrics/Get-PiHoleStatsTopDomain.ps1 | 83 +++++++++++++------ 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 index 17b78c8..6da9296 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 @@ -1,7 +1,26 @@ function Get-PiHoleStatsTopDomain { <# .SYNOPSIS -https://TODOFINDNEWAPILINK +Get top domains +Request the top domains (by query count) + +.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 MaxResult +How many results should be returned + +.PARAMETER Blocked +If true, returns top domains by blocked queries instead of total queries + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Get-PiHoleStatsTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -15,37 +34,51 @@ https://TODOFINDNEWAPILINK [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + switch ($Blocked) { + $false { $BlockedParam = "false" } + $true { $BlockedParam = "true" } + Default { throw "ERROR" } + } + Write-Verbose "Blocked: $BlockedParam" + Write-Verbose "MaxResult: $MaxResult" - switch ($Blocked) { - $false { - $Blocked = "false" + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/stats/top_domains?blocked=$BlockedParam&count=$MaxResult" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" } - $true { - $Blocked = "true" + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response } - Default { - throw "ERROR" + else { + $ObjectFinal = @() + foreach ($Item in $Response.domains) { + $Object = [PSCustomObject]@{ + Domain = $Item.domain + Count = $Item.count + } + Write-Verbose -Message "Domain - $($Item.domain): $($Item.count)" + $ObjectFinal += $Object + } + Write-Output $ObjectFinal } } - Write-Verbose "Blocked: $Blocked" - - $Params = @{ - Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/top_domains?blocked=$Blocked&count=$MaxResult" - Method = "Get" - SkipCertificateCheck = $IgnoreSsl - ContentType = "application/json" - } - - $Response = Invoke-RestMethod @Params - if ($RawOutput) { - Write-Output $Response + catch { + Write-Error -Message $_.Exception.Message } - if ($Sid) { - Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } } -} \ No newline at end of file +} From ef1ff870dd961692d8dcdb0b6f12118ce89d57fe Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:09:15 -0500 Subject: [PATCH 04/15] feat: accept local DateTime for From/Until on database stats functions Get-PiHoleStatsDatabaseSummary, Get-PiHoleStatsDatabaseUpstream, Get-PiHoleStatsDatabaseTopDomain, Get-PiHoleStatsDatabaseTopClient, and Get-PiHoleStatsDatabaseQueryType previously required callers to pass raw Unix timestamps for -From/-Until. They now accept ordinary [datetime] values and convert internally via the existing (previously unused) Convert-LocalTimeToPiHoleUnixTime helper, so callers can pass things like (Get-Date).AddDays(-7) instead of computing epoch seconds by hand. Updated the corresponding integration tests to build DateTime values instead of Unix timestamps; verified all 5 functions and their tests against a real server. Co-Authored-By: Claude Sonnet 5 --- .../Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 | 15 +++++++++------ .../Metrics/Get-PiHoleStatsDatabaseSummary.ps1 | 15 +++++++++------ .../Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 | 15 +++++++++------ .../Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 | 15 +++++++++------ .../Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 | 15 +++++++++------ ...leStatsDatabaseQueryType.Integration.Tests.ps1 | 6 +++--- ...HoleStatsDatabaseSummary.Integration.Tests.ps1 | 6 +++--- ...leStatsDatabaseTopClient.Integration.Tests.ps1 | 6 +++--- ...leStatsDatabaseTopDomain.Integration.Tests.ps1 | 6 +++--- ...oleStatsDatabaseUpstream.Integration.Tests.ps1 | 6 +++--- 10 files changed, 60 insertions(+), 45 deletions(-) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 index ab1f931..8c4cdee 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Unix timestamp from when the data should be requested +Local date/time from when the data should be requested .PARAMETER Until -Unix timestamp until when the data should be requested +Local date/time until when the data should be requested .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -26,7 +26,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From 1672580025 -Until 1672666425 +Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/query_types')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -36,9 +36,9 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" - [Parameter(Mandatory = $true)] [string]$Password, [Parameter(Mandatory = $true)] - [int]$From, + [datetime]$From, [Parameter(Mandatory = $true)] - [int]$Until, + [datetime]$Until, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) @@ -46,9 +46,12 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" - try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $FromUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $From).UnixTime + $UntilUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $Until).UnixTime + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/database/query_types?from=$From&until=$Until" + Uri = "$($PiHoleServer.OriginalString)/api/stats/database/query_types?from=$FromUnixTime&until=$UntilUnixTime" Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 index 0b5c003..7f60436 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Unix timestamp from when the data should be requested +Local date/time from when the data should be requested .PARAMETER Until -Unix timestamp until when the data should be requested +Local date/time until when the data should be requested .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -26,7 +26,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From 1672580025 -Until 1672666425 +Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/summary')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -36,9 +36,9 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Pa [Parameter(Mandatory = $true)] [string]$Password, [Parameter(Mandatory = $true)] - [int]$From, + [datetime]$From, [Parameter(Mandatory = $true)] - [int]$Until, + [datetime]$Until, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) @@ -46,9 +46,12 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Pa try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $FromUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $From).UnixTime + $UntilUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $Until).UnixTime + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/database/summary?from=$From&until=$Until" + Uri = "$($PiHoleServer.OriginalString)/api/stats/database/summary?from=$FromUnixTime&until=$UntilUnixTime" Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 index 4110f2c..d509382 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Unix timestamp from when the data should be requested +Local date/time from when the data should be requested .PARAMETER Until -Unix timestamp until when the data should be requested +Local date/time until when the data should be requested .PARAMETER MaxResult How many results should be returned @@ -32,7 +32,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From 1672580025 -Until 1672666425 -MaxResult 10 +Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/top_clients')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -42,9 +42,9 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" - [Parameter(Mandatory = $true)] [string]$Password, [Parameter(Mandatory = $true)] - [int]$From, + [datetime]$From, [Parameter(Mandatory = $true)] - [int]$Until, + [datetime]$Until, [int]$MaxResult = 10, [bool]$Blocked = $false, [bool]$IgnoreSsl = $false, @@ -62,9 +62,12 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" - Write-Verbose "Blocked: $BlockedParam" Write-Verbose "MaxResult: $MaxResult" + $FromUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $From).UnixTime + $UntilUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $Until).UnixTime + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/database/top_clients?from=$From&until=$Until&blocked=$BlockedParam&count=$MaxResult" + Uri = "$($PiHoleServer.OriginalString)/api/stats/database/top_clients?from=$FromUnixTime&until=$UntilUnixTime&blocked=$BlockedParam&count=$MaxResult" Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 index 214ec85..0b10ab0 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Unix timestamp from when the data should be requested +Local date/time from when the data should be requested .PARAMETER Until -Unix timestamp until when the data should be requested +Local date/time until when the data should be requested .PARAMETER MaxResult How many results should be returned @@ -32,7 +32,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From 1672580025 -Until 1672666425 -MaxResult 10 +Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/top_domains')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -42,9 +42,9 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" - [Parameter(Mandatory = $true)] [string]$Password, [Parameter(Mandatory = $true)] - [int]$From, + [datetime]$From, [Parameter(Mandatory = $true)] - [int]$Until, + [datetime]$Until, [int]$MaxResult = 10, [bool]$Blocked = $false, [bool]$IgnoreSsl = $false, @@ -62,9 +62,12 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" - Write-Verbose "Blocked: $BlockedParam" Write-Verbose "MaxResult: $MaxResult" + $FromUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $From).UnixTime + $UntilUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $Until).UnixTime + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/database/top_domains?from=$From&until=$Until&blocked=$BlockedParam&count=$MaxResult" + Uri = "$($PiHoleServer.OriginalString)/api/stats/database/top_domains?from=$FromUnixTime&until=$UntilUnixTime&blocked=$BlockedParam&count=$MaxResult" Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 index 8cb0d44..57d1fcb 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Unix timestamp from when the data should be requested +Local date/time from when the data should be requested .PARAMETER Until -Unix timestamp until when the data should be requested +Local date/time until when the data should be requested .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -26,7 +26,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From 1672580025 -Until 1672666425 +Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/upstreams')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -36,9 +36,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -P [Parameter(Mandatory = $true)] [string]$Password, [Parameter(Mandatory = $true)] - [int]$From, + [datetime]$From, [Parameter(Mandatory = $true)] - [int]$Until, + [datetime]$Until, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) @@ -46,9 +46,12 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -P try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + $FromUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $From).UnixTime + $UntilUnixTime = (Convert-LocalTimeToPiHoleUnixTime -Date $Until).UnixTime + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/database/upstreams?from=$From&until=$Until" + Uri = "$($PiHoleServer.OriginalString)/api/stats/database/upstreams?from=$FromUnixTime&until=$UntilUnixTime" Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 index 76f9356..eae844c 100644 --- a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 @@ -23,9 +23,9 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } - # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. - $script:Until = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $script:From = $script:Until - (30 * 86400) + # The API rejects from=0 (epoch) with a 400, so use a recent, valid window instead. + $script:Until = Get-Date + $script:From = $script:Until.AddDays(-30) } It 'returns a formatted object with all query type properties' -Skip:(-not $script:ConfigAvailable) { diff --git a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 index cca6731..15c1c0b 100644 --- a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 @@ -23,9 +23,9 @@ Describe 'Get-PiHoleStatsDatabaseSummary (Integration)' -Tag 'Integration' { & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } - # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. - $script:Until = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $script:From = $script:Until - (30 * 86400) + # The API rejects from=0 (epoch) with a 400, so use a recent, valid window instead. + $script:Until = Get-Date + $script:From = $script:Until.AddDays(-30) } It 'returns database summary as a formatted object' -Skip:(-not $script:ConfigAvailable) { diff --git a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 index abbfccf..81911c5 100644 --- a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 @@ -23,9 +23,9 @@ Describe 'Get-PiHoleStatsDatabaseTopClient (Integration)' -Tag 'Integration' { & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } - # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. - $script:Until = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $script:From = $script:Until - (30 * 86400) + # The API rejects from=0 (epoch) with a 400, so use a recent, valid window instead. + $script:Until = Get-Date + $script:From = $script:Until.AddDays(-30) } It 'returns top clients without error' -Skip:(-not $script:ConfigAvailable) { diff --git a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 index 2d5a637..5bd5b35 100644 --- a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 @@ -23,9 +23,9 @@ Describe 'Get-PiHoleStatsDatabaseTopDomain (Integration)' -Tag 'Integration' { & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } - # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. - $script:Until = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $script:From = $script:Until - (30 * 86400) + # The API rejects from=0 (epoch) with a 400, so use a recent, valid window instead. + $script:Until = Get-Date + $script:From = $script:Until.AddDays(-30) } It 'returns top domains without error' -Skip:(-not $script:ConfigAvailable) { diff --git a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 index a99fbc3..f0765be 100644 --- a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 @@ -23,9 +23,9 @@ Describe 'Get-PiHoleStatsDatabaseUpstream (Integration)' -Tag 'Integration' { & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host } - # from=0 is rejected by the API with a 400; use a wide-but-valid recent window instead. - $script:Until = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() - $script:From = $script:Until - (30 * 86400) + # The API rejects from=0 (epoch) with a 400, so use a recent, valid window instead. + $script:Until = Get-Date + $script:From = $script:Until.AddDays(-30) } It 'returns upstream metrics as a formatted object' -Skip:(-not $script:ConfigAvailable) { From caf13fd1e27af4bf32bec2ab4841e5e0879fa623 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:21:03 -0500 Subject: [PATCH 05/15] feat: make From/Until optional on database stats functions, defaulting to the last 8 hours Get-PiHoleStatsDatabaseSummary, Get-PiHoleStatsDatabaseUpstream, Get-PiHoleStatsDatabaseTopDomain, Get-PiHoleStatsDatabaseTopClient, and Get-PiHoleStatsDatabaseQueryType no longer require -From/-Until - they default to (Get-Date).AddHours(-8) and (Get-Date) respectively, matching the common case of "what's happened recently" without forcing every caller to specify a window. Added a test per function confirming the defaults work when both parameters are omitted; verified against a real server. Co-Authored-By: Claude Sonnet 5 --- .../Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 | 13 +++++++------ .../Metrics/Get-PiHoleStatsDatabaseSummary.ps1 | 13 +++++++------ .../Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 | 13 +++++++------ .../Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 | 13 +++++++------ .../Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 | 13 +++++++------ ...HoleStatsDatabaseQueryType.Integration.Tests.ps1 | 7 +++++++ ...PiHoleStatsDatabaseSummary.Integration.Tests.ps1 | 7 +++++++ ...HoleStatsDatabaseTopClient.Integration.Tests.ps1 | 5 +++++ ...HoleStatsDatabaseTopDomain.Integration.Tests.ps1 | 5 +++++ ...iHoleStatsDatabaseUpstream.Integration.Tests.ps1 | 7 +++++++ 10 files changed, 66 insertions(+), 30 deletions(-) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 index 8c4cdee..28e67c7 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Local date/time from when the data should be requested +Local date/time from when the data should be requested. Defaults to 8 hours ago. .PARAMETER Until -Local date/time until when the data should be requested +Local date/time until when the data should be requested. Defaults to now. .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -25,6 +25,9 @@ Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" + .EXAMPLE Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> @@ -35,10 +38,8 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" - [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [Parameter(Mandatory = $true)] - [datetime]$From, - [Parameter(Mandatory = $true)] - [datetime]$Until, + [datetime]$From = (Get-Date).AddHours(-8), + [datetime]$Until = (Get-Date), [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 index 7f60436..1ac523c 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Local date/time from when the data should be requested +Local date/time from when the data should be requested. Defaults to 8 hours ago. .PARAMETER Until -Local date/time until when the data should be requested +Local date/time until when the data should be requested. Defaults to now. .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -25,6 +25,9 @@ Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" + .EXAMPLE Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> @@ -35,10 +38,8 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Pa [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [Parameter(Mandatory = $true)] - [datetime]$From, - [Parameter(Mandatory = $true)] - [datetime]$Until, + [datetime]$From = (Get-Date).AddHours(-8), + [datetime]$Until = (Get-Date), [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 index d509382..a5699c9 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Local date/time from when the data should be requested +Local date/time from when the data should be requested. Defaults to 8 hours ago. .PARAMETER Until -Local date/time until when the data should be requested +Local date/time until when the data should be requested. Defaults to now. .PARAMETER MaxResult How many results should be returned @@ -31,6 +31,9 @@ Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 + .EXAMPLE Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 #> @@ -41,10 +44,8 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" - [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [Parameter(Mandatory = $true)] - [datetime]$From, - [Parameter(Mandatory = $true)] - [datetime]$Until, + [datetime]$From = (Get-Date).AddHours(-8), + [datetime]$Until = (Get-Date), [int]$MaxResult = 10, [bool]$Blocked = $false, [bool]$IgnoreSsl = $false, diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 index 0b10ab0..3bcf4fb 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Local date/time from when the data should be requested +Local date/time from when the data should be requested. Defaults to 8 hours ago. .PARAMETER Until -Local date/time until when the data should be requested +Local date/time until when the data should be requested. Defaults to now. .PARAMETER MaxResult How many results should be returned @@ -31,6 +31,9 @@ Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 + .EXAMPLE Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 #> @@ -41,10 +44,8 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" - [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [Parameter(Mandatory = $true)] - [datetime]$From, - [Parameter(Mandatory = $true)] - [datetime]$Until, + [datetime]$From = (Get-Date).AddHours(-8), + [datetime]$Until = (Get-Date), [int]$MaxResult = 10, [bool]$Blocked = $false, [bool]$IgnoreSsl = $false, diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 index 57d1fcb..3175631 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 @@ -14,10 +14,10 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h The API Password you generated from your PiHole server .PARAMETER From -Local date/time from when the data should be requested +Local date/time from when the data should be requested. Defaults to 8 hours ago. .PARAMETER Until -Local date/time until when the data should be requested +Local date/time until when the data should be requested. Defaults to now. .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -25,6 +25,9 @@ Set to $true to skip SSL certificate validation .PARAMETER RawOutput This will dump the response instead of the formatted object +.EXAMPLE +Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" + .EXAMPLE Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> @@ -35,10 +38,8 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -P [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, - [Parameter(Mandatory = $true)] - [datetime]$From, - [Parameter(Mandatory = $true)] - [datetime]$Until, + [datetime]$From = (Get-Date).AddHours(-8), + [datetime]$Until = (Get-Date), [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) diff --git a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 index eae844c..6dd65a8 100644 --- a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 @@ -49,4 +49,11 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { $errOut | Should -Not -BeNullOrEmpty } + + It 'defaults to the last 8 hours when From/Until are omitted' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsDatabaseQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } } diff --git a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 index 15c1c0b..4027ca6 100644 --- a/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseSummary.Integration.Tests.ps1 @@ -49,4 +49,11 @@ Describe 'Get-PiHoleStatsDatabaseSummary (Integration)' -Tag 'Integration' { $errOut | Should -Not -BeNullOrEmpty } + + It 'defaults to the last 8 hours when From/Until are omitted' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsDatabaseSummary -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } } diff --git a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 index 81911c5..9d7120f 100644 --- a/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopClient.Integration.Tests.ps1 @@ -45,4 +45,9 @@ Describe 'Get-PiHoleStatsDatabaseTopClient (Integration)' -Tag 'Integration' { $errOut | Should -Not -BeNullOrEmpty } + + It 'defaults to the last 8 hours when From/Until are omitted' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsDatabaseTopClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host + } } diff --git a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 index 5bd5b35..42b34ee 100644 --- a/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseTopDomain.Integration.Tests.ps1 @@ -45,4 +45,9 @@ Describe 'Get-PiHoleStatsDatabaseTopDomain (Integration)' -Tag 'Integration' { $errOut | Should -Not -BeNullOrEmpty } + + It 'defaults to the last 8 hours when From/Until are omitted' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host + } } diff --git a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 index f0765be..83ba3ac 100644 --- a/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseUpstream.Integration.Tests.ps1 @@ -49,4 +49,11 @@ Describe 'Get-PiHoleStatsDatabaseUpstream (Integration)' -Tag 'Integration' { $errOut | Should -Not -BeNullOrEmpty } + + It 'defaults to the last 8 hours when From/Until are omitted' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsDatabaseUpstream -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } } From d7f9398e0a054ad5e93ee16108aa77cffb49d88d Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:21:19 -0500 Subject: [PATCH 06/15] fix: add explicit RawOutput = $false default for consistency Get-PiHoleStatsSummary and Get-PiHoleStatsRecentBlocked declared [bool]$RawOutput with no explicit default - functionally identical to $false since that's bool's default value, but every other function in the module states it explicitly. Found while auditing all functions' RawOutput declarations for consistency. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 index b390fcd..0494af4 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 @@ -27,7 +27,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer "http://pihole.domain.com:8080" -Pass [string]$Password, [int]$MaxResult = 1, [bool]$IgnoreSsl = $false, - [bool]$RawOutput + [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 index 17c8274..193ab1d 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 @@ -27,7 +27,7 @@ Get-PiHoleStatsSummary -PiHoleServer "http://pihole.domain.com:8080" -Password " [Parameter(Mandatory = $true)] [string]$Password, [bool]$IgnoreSsl = $false, - [bool]$RawOutput + [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl From 7b4ddb982776d4a44a9c1d3db427111624edb5d1 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:21:44 -0500 Subject: [PATCH 07/15] feat: make Set-PiHoleDnsBlocking's TimeInSeconds a required parameter It previously defaulted to $null, but since the parameter is typed [int], PowerShell silently coerced that to 0 rather than actually omitting a timer value - contradicting the old docstring's claim that omitting it made the setting permanent. Making it mandatory forces callers to state their intent explicitly instead of relying on that silent coercion. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 b/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 index ab64125..09364e0 100644 --- a/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 +++ b/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 @@ -13,7 +13,7 @@ The API Password you generated from your PiHole server True or False, if you set it to False when Blocking was set to true, it will disable blocking .PARAMETER TimeInSeconds -How long should the opposite setting last, if you do not set a time, it will be set forever until you change it +How long the opposite setting should last, in seconds .PARAMETER RawOutput This will dump the response instead of the formatted object @@ -31,7 +31,8 @@ Set-PiHoleDnsBlocking -PiHoleServer "http://pihole.domain.com:8080" -Password "f [string]$Password, [ValidateSet("True", "False")] $Blocking, - [int]$TimeInSeconds = $null, + [Parameter(Mandatory = $true)] + [int]$TimeInSeconds, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) From e6d98c6aee901db785274d7bd50dee46a5d20925 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:33:08 -0500 Subject: [PATCH 08/15] feat: restructure QueryType functions as Type/Count row arrays Get-PiHoleStatsQueryType and Get-PiHoleStatsDatabaseQueryType previously returned a single wide object with A/AAAA/ANY/... as top-level properties. They now return an array of {Type, Count} objects instead, matching the row-based tabular pattern already used by the Domain/Count and IP/Name/Count functions in this folder - sortable and filterable with Sort-Object/Where-Object instead of having to know all 16 property names up front. While rewriting Get-PiHoleStatsQueryType, also fixed it to use the module's standard try/catch/finally pattern with session cleanup, which it was missing entirely, plus a stray reference to an undefined $MaxResult variable in a Write-Verbose call. Updated the corresponding integration test's assertions to match the new array shape; verified both functions against a real server. Co-Authored-By: Claude Sonnet 5 --- .../Get-PiHoleStatsDatabaseQueryType.ps1 | 29 ++----- .../Metrics/Get-PiHoleStatsQueryType.ps1 | 84 +++++++++++-------- ...atsDatabaseQueryType.Integration.Tests.ps1 | 10 +-- 3 files changed, 64 insertions(+), 59 deletions(-) diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 index 28e67c7..5350d52 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseQueryType.ps1 @@ -26,10 +26,10 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" .EXAMPLE -Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) +Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/query_types')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -64,25 +64,14 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer "http://pihole.domain.com:8080" - Write-Output $Response } else { - $Object = [PSCustomObject]@{ - A = $Response.types.A - AAAA = $Response.types.AAAA - ANY = $Response.types.ANY - SRV = $Response.types.SRV - SOA = $Response.types.SOA - PTR = $Response.types.PTR - TXT = $Response.types.TXT - NAPTR = $Response.types.NAPTR - MX = $Response.types.MX - DS = $Response.types.DS - RRSIG = $Response.types.RRSIG - DNSKEY = $Response.types.DNSKEY - NS = $Response.types.NS - SVCB = $Response.types.SVCB - HTTPS = $Response.types.HTTPS - OTHER = $Response.types.OTHER + $QueryTypes = @('A', 'AAAA', 'ANY', 'SRV', 'SOA', 'PTR', 'TXT', 'NAPTR', 'MX', 'DS', 'RRSIG', 'DNSKEY', 'NS', 'SVCB', 'HTTPS', 'OTHER') + $ObjectFinal = foreach ($Type in $QueryTypes) { + [PSCustomObject]@{ + Type = $Type + Count = $Response.types.$Type + } } - Write-Output $Object + Write-Output $ObjectFinal } } diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsQueryType.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsQueryType.ps1 index 2cc78e7..1117f7e 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsQueryType.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsQueryType.ps1 @@ -1,7 +1,23 @@ function Get-PiHoleStatsQueryType { <# .SYNOPSIS -https://TODOFINDNEWAPILINK +Get query types +Request a breakdown of query types (A, AAAA, ...) + +.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 response instead of the formatted object + +.EXAMPLE +Get-PiHoleStatsQueryType -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -14,41 +30,41 @@ https://TODOFINDNEWAPILINK [bool]$RawOutput = $false ) - $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - Write-Verbose -Message "MaxResults - $MaxResult" - $Params = @{ - Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/stats/query_types" - Method = "Get" - SkipCertificateCheck = $IgnoreSsl - ContentType = "application/json" - } + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl - $Response = Invoke-RestMethod @Params + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = "$($PiHoleServer.OriginalString)/api/stats/query_types" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } - if ($RawOutput) { - Write-Output $Response + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $QueryTypes = @('A', 'AAAA', 'ANY', 'SRV', 'SOA', 'PTR', 'TXT', 'NAPTR', 'MX', 'DS', 'RRSIG', 'DNSKEY', 'NS', 'SVCB', 'HTTPS', 'OTHER') + $ObjectFinal = foreach ($Type in $QueryTypes) { + [PSCustomObject]@{ + Type = $Type + Count = $Response.types.$Type + } + } + Write-Output $ObjectFinal + } } - else { - $Object = [PSCustomObject]@{ - A = $Response.types.A - AAAA = $Response.types.AAAA - ANY = $Response.types.ANY - SRV = $Response.types.SRV - SOA = $Response.types.SOA - PTR = $Response.types.PTR - TXT = $Response.types.TXT - NAPTR = $Response.types.NAPTR - MX = $Response.types.MX - DS = $Response.types.DS - RRSIG = $Response.types.RRSIG - DNSKEY = $Response.types.DNSKEY - NS = $Response.types.NS - SVCB = $Response.types.SVCB - HTTPS = $Response.types.HTTPS - OTHER = $Response.types.OTHER + + catch { + Write-Error -Message $_.Exception.Message + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } - $ObjectFinal += $Object - Write-Output $ObjectFinal } -} \ No newline at end of file +} diff --git a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 index 6dd65a8..6bcc515 100644 --- a/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 +++ b/tests/Get-PiHoleStatsDatabaseQueryType.Integration.Tests.ps1 @@ -28,13 +28,13 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { $script:From = $script:Until.AddDays(-30) } - It 'returns a formatted object with all query type properties' -Skip:(-not $script:ConfigAvailable) { + It 'returns an array of Type/Count rows for every query type' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -From $script:From -Until $script:Until -IgnoreSsl $script:PiHoleIgnoreSsl - $result | Format-List | Out-String | Write-Host + $result | Format-Table | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty - $result.PSObject.Properties.Name | Should -Contain 'A' - $result.PSObject.Properties.Name | Should -Contain 'AAAA' + ($result | Where-Object Type -EQ 'A') | Should -Not -BeNullOrEmpty + ($result | Where-Object Type -EQ 'AAAA') | Should -Not -BeNullOrEmpty } It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { @@ -52,7 +52,7 @@ Describe 'Get-PiHoleStatsDatabaseQueryType (Integration)' -Tag 'Integration' { It 'defaults to the last 8 hours when From/Until are omitted' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleStatsDatabaseQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl - $result | Format-List | Out-String | Write-Host + $result | Format-Table | Out-String | Write-Host $result | Should -Not -BeNullOrEmpty } From b7d4bd77ab00caeed473ab8e24588287cc7ad497 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:33:30 -0500 Subject: [PATCH 09/15] docs: use a consistent, simpler placeholder password in examples Every .EXAMPLE block used the same made-up gibberish string "fjdsjfldsjfkldjslafjskdl" for -Password. Replaced it with "your-app-password" everywhere for clarity. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 | 2 +- PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 | 2 +- .../Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 | 2 +- .../Public/Authentication/Remove-PiHoleAuthSession.ps1 | 2 +- PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 | 2 +- PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 | 2 +- PiHoleShell/Public/ListManagement/Search-PiHoleListDomain.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 | 4 ++-- .../Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 | 4 ++-- .../Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 | 4 ++-- .../Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 | 4 ++-- .../Public/Metrics/Get-PiHoleStatsQuerySuggestions.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsTopClient.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 | 2 +- PiHoleShell/Public/Metrics/Get-PiHoleStatsUpstream.ps1 | 2 +- 17 files changed, 21 insertions(+), 21 deletions(-) diff --git a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 index e1b6a8c..2c47c34 100644 --- a/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 +++ b/PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1 @@ -19,7 +19,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Invoke-PiHoleFlushNetwork -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/flush/network')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Flushes PiHole logs')] diff --git a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 index 2257393..60d8c7d 100644 --- a/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 +++ b/PiHoleShell/Public/Actions/Restart-PiHoleDnsService.ps1 @@ -19,7 +19,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Invoke-PiHoleRestartDns -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/restartdns')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Restarts PiHole DNS')] diff --git a/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 b/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 index 2ae3603..fdce234 100644 --- a/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 +++ b/PiHoleShell/Public/Authentication/Get-PiHoleCurrentAuthSession.ps1 @@ -16,7 +16,7 @@ Ignore SSL when interacting with the PiHole API This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/auth/sessions')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Authentication/Remove-PiHoleAuthSession.ps1 b/PiHoleShell/Public/Authentication/Remove-PiHoleAuthSession.ps1 index c448ad7..e34fff4 100644 --- a/PiHoleShell/Public/Authentication/Remove-PiHoleAuthSession.ps1 +++ b/PiHoleShell/Public/Authentication/Remove-PiHoleAuthSession.ps1 @@ -13,7 +13,7 @@ The API Password you generated from your PiHole server Ignore SSL when interacting with the PiHole API .EXAMPLE -Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleCurrentAuthSession -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not change state')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 b/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 index e27a504..804b3e7 100644 --- a/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 +++ b/PiHoleShell/Public/DnsControl/Get-PiHoleDnsBlockingStatus.ps1 @@ -16,7 +16,7 @@ Ignore SSL when interacting with the PiHole API This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleDnsBlockingStatus -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleDnsBlockingStatus -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( diff --git a/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 b/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 index 09364e0..de140d2 100644 --- a/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 +++ b/PiHoleShell/Public/DnsControl/Set-PiHoleDnsBlocking.ps1 @@ -19,7 +19,7 @@ How long the opposite setting should last, in seconds This will dump the response instead of the formatted object .EXAMPLE -Set-PiHoleDnsBlocking -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -Blocking $false -TimeInSeconds 60 +Set-PiHoleDnsBlocking -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Blocking $false -TimeInSeconds 60 #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not change state')] diff --git a/PiHoleShell/Public/ListManagement/Search-PiHoleListDomain.ps1 b/PiHoleShell/Public/ListManagement/Search-PiHoleListDomain.ps1 index 87374c3..4d8c7de 100644 --- a/PiHoleShell/Public/ListManagement/Search-PiHoleListDomain.ps1 +++ b/PiHoleShell/Public/ListManagement/Search-PiHoleListDomain.ps1 @@ -25,7 +25,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Search-PiHoleListDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -Domain "doubleclick.net" +Search-PiHoleListDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Domain "doubleclick.net" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/search/-domain-')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 index 1ac523c..54bc13c 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseSummary.ps1 @@ -26,10 +26,10 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" .EXAMPLE -Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) +Get-PiHoleStatsDatabaseSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/summary')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 index a5699c9..4125f56 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopClient.ps1 @@ -32,10 +32,10 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 +Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxResult 10 .EXAMPLE -Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 +Get-PiHoleStatsDatabaseTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/top_clients')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 index 3bcf4fb..67c4dcd 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseTopDomain.ps1 @@ -32,10 +32,10 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 +Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxResult 10 .EXAMPLE -Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 +Get-PiHoleStatsDatabaseTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -From (Get-Date).AddDays(-7) -Until (Get-Date) -MaxResult 10 #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/top_domains')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 index 3175631..0d35e26 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsDatabaseUpstream.ps1 @@ -26,10 +26,10 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" .EXAMPLE -Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -From (Get-Date).AddDays(-7) -Until (Get-Date) +Get-PiHoleStatsDatabaseUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -From (Get-Date).AddDays(-7) -Until (Get-Date) #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/database/upstreams')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsQuerySuggestions.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsQuerySuggestions.ps1 index 245bdb4..30421bf 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsQuerySuggestions.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsQuerySuggestions.ps1 @@ -20,7 +20,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsQuerySuggestions -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleStatsQuerySuggestions -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/queries/suggestions')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 index 0494af4..e8104fc 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsRecentBlocked.ps1 @@ -16,7 +16,7 @@ How many results should be returned This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsRecentBlocked -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 20 +Get-PiHoleStatsRecentBlocked -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxResult 20 #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/recent_blocked')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 index 193ab1d..18c1fbb 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsSummary.ps1 @@ -17,7 +17,7 @@ This will dump the response instead of the formatted object This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleStatsSummary -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopClient.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopClient.ps1 index eba3963..79a43e5 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopClient.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopClient.ps1 @@ -20,7 +20,7 @@ If true, returns top clients by blocked queries instead of total queries This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 +Get-PiHoleStatsTopClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxResult 10 #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 index 6da9296..81031ef 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsTopDomain.ps1 @@ -20,7 +20,7 @@ If true, returns top domains by blocked queries instead of total queries This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" -MaxResult 10 +Get-PiHoleStatsTopDomain -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxResult 10 #> [CmdletBinding()] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] diff --git a/PiHoleShell/Public/Metrics/Get-PiHoleStatsUpstream.ps1 b/PiHoleShell/Public/Metrics/Get-PiHoleStatsUpstream.ps1 index 4ec0e36..52ede7b 100644 --- a/PiHoleShell/Public/Metrics/Get-PiHoleStatsUpstream.ps1 +++ b/PiHoleShell/Public/Metrics/Get-PiHoleStatsUpstream.ps1 @@ -20,7 +20,7 @@ Set to $true to skip SSL certificate validation This will dump the response instead of the formatted object .EXAMPLE -Get-PiHoleStatsUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "fjdsjfldsjfkldjslafjskdl" +Get-PiHoleStatsUpstream -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/stats/upstreams')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] From 82a6bba05dd5bd3e92b9a45335cf51b64a98a076 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:54:55 -0500 Subject: [PATCH 10/15] test: add integration tests for the remaining Get-* functions Adds real-server integration tests for every exported Get-* function that didn't already have one: Get-PiHoleCurrentAuthSession, Get-PiHoleConfig, Get-PiHoleDnsBlockingStatus, Get-PiHoleInfoHost, Get-PiHoleInfoMessage, Get-PiHoleGroup, Get-PiHoleList, Get-PiHoleStatsQueryType (live), Get-PiHoleStatsRecentBlocked, Get-PiHoleStatsSummary, Get-PiHoleStatsTopClient, Get-PiHoleStatsTopDomain, and Get-PiHolePadd - 39 tests total, all passing against a real server. Get-PiHoleLogWebserver and Get-PiHoleTeleporterDownload are excluded: both exist as Public/ functions but aren't in the module's export list, so they can't actually be called after Import-Module. Co-Authored-By: Claude Sonnet 5 --- tests/Get-PiHoleConfig.Integration.Tests.ps1 | 43 +++++++++++++++++ ...leCurrentAuthSession.Integration.Tests.ps1 | 41 ++++++++++++++++ ...oleDnsBlockingStatus.Integration.Tests.ps1 | 42 ++++++++++++++++ tests/Get-PiHoleGroup.Integration.Tests.ps1 | 42 ++++++++++++++++ .../Get-PiHoleInfoHost.Integration.Tests.ps1 | 42 ++++++++++++++++ ...et-PiHoleInfoMessage.Integration.Tests.ps1 | 41 ++++++++++++++++ tests/Get-PiHoleList.Integration.Tests.ps1 | 41 ++++++++++++++++ tests/Get-PiHolePadd.Integration.Tests.ps1 | 46 ++++++++++++++++++ ...PiHoleStatsQueryType.Integration.Tests.ps1 | 46 ++++++++++++++++++ ...leStatsRecentBlocked.Integration.Tests.ps1 | 45 +++++++++++++++++ ...t-PiHoleStatsSummary.Integration.Tests.ps1 | 48 +++++++++++++++++++ ...PiHoleStatsTopClient.Integration.Tests.ps1 | 44 +++++++++++++++++ ...PiHoleStatsTopDomain.Integration.Tests.ps1 | 44 +++++++++++++++++ 13 files changed, 565 insertions(+) create mode 100644 tests/Get-PiHoleConfig.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleCurrentAuthSession.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleDnsBlockingStatus.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleGroup.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleInfoHost.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleInfoMessage.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleList.Integration.Tests.ps1 create mode 100644 tests/Get-PiHolePadd.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleStatsQueryType.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleStatsRecentBlocked.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleStatsTopClient.Integration.Tests.ps1 create mode 100644 tests/Get-PiHoleStatsTopDomain.Integration.Tests.ps1 diff --git a/tests/Get-PiHoleConfig.Integration.Tests.ps1 b/tests/Get-PiHoleConfig.Integration.Tests.ps1 new file mode 100644 index 0000000..0f6dc74 --- /dev/null +++ b/tests/Get-PiHoleConfig.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. + +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleConfig (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns config as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Dns | Should -Not -BeNullOrEmpty + $result.Dhcp | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.config | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleCurrentAuthSession.Integration.Tests.ps1 b/tests/Get-PiHoleCurrentAuthSession.Integration.Tests.ps1 new file mode 100644 index 0000000..64f29d4 --- /dev/null +++ b/tests/Get-PiHoleCurrentAuthSession.Integration.Tests.ps1 @@ -0,0 +1,41 @@ +# 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 'Get-PiHoleCurrentAuthSession (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns active sessions as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleCurrentAuthSession -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleCurrentAuthSession -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleCurrentAuthSession -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleDnsBlockingStatus.Integration.Tests.ps1 b/tests/Get-PiHoleDnsBlockingStatus.Integration.Tests.ps1 new file mode 100644 index 0000000..23592c1 --- /dev/null +++ b/tests/Get-PiHoleDnsBlockingStatus.Integration.Tests.ps1 @@ -0,0 +1,42 @@ +# 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 'Get-PiHoleDnsBlockingStatus (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns blocking status as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsBlockingStatus -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Blocking | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsBlockingStatus -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.blocking | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleDnsBlockingStatus -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleGroup.Integration.Tests.ps1 b/tests/Get-PiHoleGroup.Integration.Tests.ps1 new file mode 100644 index 0000000..89c564f --- /dev/null +++ b/tests/Get-PiHoleGroup.Integration.Tests.ps1 @@ -0,0 +1,42 @@ +# 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 'Get-PiHoleGroup (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns groups as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + ($result | Where-Object Name -EQ 'Default') | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 b/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 new file mode 100644 index 0000000..0aa7b03 --- /dev/null +++ b/tests/Get-PiHoleInfoHost.Integration.Tests.ps1 @@ -0,0 +1,42 @@ +# 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 'Get-PiHoleInfoHost (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns host info as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleInfoHost -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.NodeName | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleInfoHost -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleInfoHost -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleInfoMessage.Integration.Tests.ps1 b/tests/Get-PiHoleInfoMessage.Integration.Tests.ps1 new file mode 100644 index 0000000..4acc18f --- /dev/null +++ b/tests/Get-PiHoleInfoMessage.Integration.Tests.ps1 @@ -0,0 +1,41 @@ +# 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 'Get-PiHoleInfoMessage (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + # Diagnosis messages may legitimately be empty on a healthy server, so this only asserts + # that the call succeeds, not that any messages exist. + It 'returns diagnosis messages without error' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleInfoMessage -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleInfoMessage -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleInfoMessage -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleList.Integration.Tests.ps1 b/tests/Get-PiHoleList.Integration.Tests.ps1 new file mode 100644 index 0000000..cfe219f --- /dev/null +++ b/tests/Get-PiHoleList.Integration.Tests.ps1 @@ -0,0 +1,41 @@ +# 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 'Get-PiHoleList (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + } + } + + It 'returns lists as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleList -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleList -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHolePadd.Integration.Tests.ps1 b/tests/Get-PiHolePadd.Integration.Tests.ps1 new file mode 100644 index 0000000..c03db0d --- /dev/null +++ b/tests/Get-PiHolePadd.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. + +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHolePadd (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host + } + } + + It 'returns the PADD dashboard data as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHolePadd -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.NodeName | Should -Not -BeNullOrEmpty + $result.Queries | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHolePadd -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHolePadd -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleStatsQueryType.Integration.Tests.ps1 b/tests/Get-PiHoleStatsQueryType.Integration.Tests.ps1 new file mode 100644 index 0000000..07cbbf4 --- /dev/null +++ b/tests/Get-PiHoleStatsQueryType.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. + +$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1') + +Describe 'Get-PiHoleStatsQueryType (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host + } + } + + It 'returns an array of Type/Count rows for every query type' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + ($result | Where-Object Type -EQ 'A') | Should -Not -BeNullOrEmpty + ($result | Where-Object Type -EQ 'AAAA') | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsQueryType -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.types | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsQueryType -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleStatsRecentBlocked.Integration.Tests.ps1 b/tests/Get-PiHoleStatsRecentBlocked.Integration.Tests.ps1 new file mode 100644 index 0000000..7e9958a --- /dev/null +++ b/tests/Get-PiHoleStatsRecentBlocked.Integration.Tests.ps1 @@ -0,0 +1,45 @@ +# 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 'Get-PiHoleStatsRecentBlocked (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic (including known ad/tracker domains that get + # blocked) so this function has something recent to report. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host + } + } + + It 'returns recently blocked domains as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsRecentBlocked -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsRecentBlocked -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.blocked | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsRecentBlocked -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 b/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 new file mode 100644 index 0000000..00aa057 --- /dev/null +++ b/tests/Get-PiHoleStatsSummary.Integration.Tests.ps1 @@ -0,0 +1,48 @@ +# 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 'Get-PiHoleStatsSummary (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host + } + } + + It 'returns a summary as a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsSummary -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Total | Should -BeGreaterOrEqual 0 + $result.Types | Should -Not -BeNullOrEmpty + $result.Status | Should -Not -BeNullOrEmpty + $result.Replies | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsSummary -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.queries | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsSummary -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleStatsTopClient.Integration.Tests.ps1 b/tests/Get-PiHoleStatsTopClient.Integration.Tests.ps1 new file mode 100644 index 0000000..7f8fc67 --- /dev/null +++ b/tests/Get-PiHoleStatsTopClient.Integration.Tests.ps1 @@ -0,0 +1,44 @@ +# 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 'Get-PiHoleStatsTopClient (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host + } + } + + It 'returns top clients as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsTopClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsTopClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.clients | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsTopClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Get-PiHoleStatsTopDomain.Integration.Tests.ps1 b/tests/Get-PiHoleStatsTopDomain.Integration.Tests.ps1 new file mode 100644 index 0000000..e09aa23 --- /dev/null +++ b/tests/Get-PiHoleStatsTopDomain.Integration.Tests.ps1 @@ -0,0 +1,44 @@ +# 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 'Get-PiHoleStatsTopDomain (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1' + if (Test-Path $configPath) { + . $configPath + $script:PiHoleServer = $PiHoleServer + $script:PiHoleToken = $PiHoleToken + $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl + + # Generates some real query traffic so live stats aren't all zero/empty. + & (Join-Path $PSScriptRoot 'Initialize-PiHoleTestData.ps1') -DnsServer $PiHoleServer.Host + } + } + + It 'returns top domains as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsTopDomain -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxResult 5 + $result | Format-Table | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsTopDomain -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.domains | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleStatsTopDomain -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} From ff504db121f73589030379ae6cdc3830c0ba6f5e Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:55:05 -0500 Subject: [PATCH 11/15] fix: replace TODO docstring on Get-PiHolePadd and fix Evicted typo Get-PiHolePadd was the only function left in the module with a placeholder ".SYNOPSIS https://TODO" docstring. Replaced it with a proper synopsis/description/parameter block matching the API spec's actual summary ("Get summarized data for PADD") and added a HelpUri, following the same convention as every other function. Also fixed a real bug found while rewriting it: Cache.Evicted read from $Reponse.cache.evicted (missing 's'), an undefined variable, so it was always $null regardless of the actual value. Verified against a real server that Evicted now returns the real value. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 | 26 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 b/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 index 5186fca..662266f 100644 --- a/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 +++ b/PiHoleShell/Public/Padd/Get-PiHolePadd.ps1 @@ -1,11 +1,29 @@ function Get-PiHolePadd { <# .SYNOPSIS -https://TODO +Get summarized data for PADD +.DESCRIPTION +Request the summarized dashboard data used to power PADD (the Pi-hole ASCII dashboard): +current CPU/memory load, blocking status, gravity size, network interfaces, query totals, +sensors, and top clients/domains/blocked entries. + +.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 response instead of the formatted object + +.EXAMPLE +Get-PiHolePadd -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" #> - #Work In Progress - [CmdletBinding()] + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/padd')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( [Parameter(Mandatory = $true)] @@ -74,7 +92,7 @@ https://TODO $Cache = [PSCustomObject]@{ Size = $Response.cache.size Inserted = $Response.cache.inserted - Evicted = $Reponse.cache.evicted + Evicted = $Response.cache.evicted } $Object = [PSCustomObject]@{ From dc59fa957df6348bfd8a65c01e1cdbc300199be2 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sat, 19 Sep 2026 23:55:18 -0500 Subject: [PATCH 12/15] fix: sync PowerShell Gallery release notes with GitHub's The module manifest's PSData.ReleaseNotes field was a static placeholder ("Initial release targeting PowerShell 7+") that never got updated, so the PowerShell Gallery package page always showed that instead of anything about the actual release - unlike the GitHub release, which auto-generates real "What's Changed" notes. Generates the release notes once via the GitHub API (releases/generate-notes) right after tagging, then uses that same text for both the GitHub release body (via body_path, replacing generate_release_notes: true) and the manifest's ReleaseNotes field before Publish-Module runs, so both places show identical content. The manifest update uses a surgical literal-text replace (matching the existing lightweight ModuleVersion bump pattern already used in this workflow) rather than Update-ModuleManifest, which rewrites and reformats the entire file - verified locally that Update-ModuleManifest drops comments, changes array literal styles, and silently comments out VariablesToExport, none of which the targeted replace does. Also removed a latent bug in the Publish step: it referenced $ENV:TAG, which was never set (the actual env var is NEW_TAG), so that redundant version-replace was already a silent no-op - the "Zip folder" step's replace was the only one ever taking effect. Verified locally: ran the full Zip-then-Publish manifest-editing sequence against a scratch copy, including release notes containing an apostrophe and a literal "$variable"-looking string, and confirmed Test-ModuleManifest validates the result cleanly. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/CreateRelease.yml | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml index b5d58cf..7ee89de 100644 --- a/.github/workflows/CreateRelease.yml +++ b/.github/workflows/CreateRelease.yml @@ -51,6 +51,16 @@ jobs: git tag ${{ steps.bump.outputs.new_tag }} git push origin ${{ steps.bump.outputs.new_tag }} + - name: Generate release notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh api "repos/${{ github.repository }}/releases/generate-notes" \ + -f tag_name="${{ steps.bump.outputs.new_tag }}" \ + -f previous_tag_name="${{ steps.get_tag.outputs.latest_tag }}" \ + --jq .body > release-notes.md + cat release-notes.md + - name: Copy README into module folder shell: pwsh run: | @@ -82,7 +92,7 @@ jobs: with: tag_name: ${{ steps.bump.outputs.new_tag }} name: "Release ${{ steps.bump.outputs.new_tag }}" - generate_release_notes: true + body_path: release-notes.md files: output/release.zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -94,14 +104,19 @@ jobs: $modulePath = "PiHoleShell" # Change to your actual module folder $manifest = Get-ChildItem -Path $modulePath -Filter PiHoleShell.psd1 -Recurse | Select-Object -First 1 - (Get-Content $($manifest.fullname)) -replace '0.0.0', ($ENV:TAG -replace "v", "") | Out-File $manifest.fullname - Write-Host "Replacing 0.0.0 with $ENV:NEW_TAG" - if (-not $manifest) { throw "No module manifest (*.psd1) found in $modulePath" } - + + # Surgical replace rather than Update-ModuleManifest, which rewrites and reformats + # the entire file (drops comments, changes array literal styles, etc.). The source + # manifest always has this exact static placeholder, since the version/notes bump + # here is never committed back to the repo. + $releaseNotes = (Get-Content -Path release-notes.md -Raw).Trim() + $releaseNotesEscaped = $releaseNotes -replace "'", "''" + $manifestContent = Get-Content -Path $manifest.FullName -Raw + $manifestContent = $manifestContent.Replace("'Initial release targeting PowerShell 7+'", "'$releaseNotesEscaped'") + Set-Content -Path $manifest.FullName -Value $manifestContent -NoNewline + Write-Host "Publishing module: $($manifest.FullName)" - Publish-Module -Path $manifest.DirectoryName -NuGetApiKey $apiKey -Verbose - env: - NEW_TAG: ${{ steps.bump.outputs.new_tag }} \ No newline at end of file + Publish-Module -Path $manifest.DirectoryName -NuGetApiKey $apiKey -Verbose \ No newline at end of file From 2fbe771b5edf4a77b8881c6ed5ec3c09c38c6e25 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 00:46:17 -0500 Subject: [PATCH 13/15] feat: make Get-PiHoleConfig return the full config tree, PascalCased Get-PiHoleConfig's formatted output only ever captured Dns.Upstreams and the dhcp section - the API actually returns 9 top-level sections (dns, dhcp, ntp, resolver, database, webserver, files, misc, debug), each several levels deep, and everything outside those two hand-picked fields was silently discarded. Rather than hardcode this large, evolving tree by hand, added a generic recursive converter (ConvertTo-PiHolePascalCaseObject) that rebuilds any API response as nested PSCustomObjects/arrays with PascalCase property names - splitting on underscores and capitalizing each segment, matching the convention already used elsewhere in the module (EXTERNAL_BLOCKED_IP -> ExternalBlockedIp). Get-PiHoleConfig now returns the entire config faithfully instead of a lossy subset, and any new config keys Pi-hole adds in the future show up automatically instead of needing code changes. Also replaced its placeholder docstring (bare HelpUri, no parameters) with a proper one, and dropped the stray `break` in its catch block to match the module's standard error-handling pattern. Verified against a real server: all 9 top-level sections present, correct PascalCase for camelCase (ignoreLocalhost -> IgnoreLocalhost), snake_case (app_pwhash -> AppPwhash, client_history_global_max -> ClientHistoryGlobalMax), and acronym-heavy names (CNAMEdeepInspect, EDNS0ECS, blockESNI -> BlockESNI) left intact. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Private/Misc.ps1 | 51 +++++++++++++++ .../Public/Config/Get-PiHoleConfig.ps1 | 63 +++++++++---------- 2 files changed, 80 insertions(+), 34 deletions(-) diff --git a/PiHoleShell/Private/Misc.ps1 b/PiHoleShell/Private/Misc.ps1 index c9c50cc..3b28ba6 100644 --- a/PiHoleShell/Private/Misc.ps1 +++ b/PiHoleShell/Private/Misc.ps1 @@ -47,6 +47,57 @@ function Convert-LocalTimeToPiHoleUnixTime { Write-Output $ObjectFinal } +function ConvertTo-PiHolePascalCase { + #INTERNAL FUNCTION + param ( + [string]$Name + ) + + if ([string]::IsNullOrEmpty($Name)) { + return $Name + } + + $Segments = $Name -split '_' | Where-Object { $_.Length -gt 0 } + $PascalSegments = foreach ($Segment in $Segments) { + $Segment.Substring(0, 1).ToUpper() + $Segment.Substring(1) + } + return ($PascalSegments -join '') +} + +function ConvertTo-PiHolePascalCaseObject { + #INTERNAL FUNCTION + # + # Recursively rebuilds an API response as nested PSCustomObjects/arrays with PascalCase + # property names (e.g. EXTERNAL_BLOCKED_IP / app_pwhash -> ExternalBlockedIp / AppPwhash), + # so deep/wide response trees don't need every field hardcoded by hand to be PowerShell + # object friendly - and so newly added API fields show up automatically instead of being + # silently dropped. + param ( + [Parameter(ValueFromPipeline = $true)] + $InputObject + ) + process { + if ($null -eq $InputObject) { + return $null + } + + if ($InputObject -is [System.Management.Automation.PSCustomObject]) { + $Result = [ordered]@{} + foreach ($Prop in $InputObject.PSObject.Properties) { + $Key = ConvertTo-PiHolePascalCase -Name $Prop.Name + $Result[$Key] = ConvertTo-PiHolePascalCaseObject -InputObject $Prop.Value + } + return [PSCustomObject]$Result + } + + if (($InputObject -is [System.Collections.IEnumerable]) -and ($InputObject -isnot [string])) { + return @($InputObject | ForEach-Object { ConvertTo-PiHolePascalCaseObject -InputObject $_ }) + } + + return $InputObject + } +} + function Remove-PiHoleCurrentAuthSession { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")] [CmdletBinding()] diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 index 5a2ace8..12cb171 100644 --- a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 +++ b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 @@ -1,11 +1,33 @@ function Get-PiHoleConfig { <# .SYNOPSIS -https://ftl.pi-hole.net/master/docs/#get-/config +Get current configuration of Pi-hole +.DESCRIPTION +Request Pi-hole's full configuration tree (dns, dhcp, ntp, resolver, database, webserver, +files, misc, and debug settings). The formatted output mirrors the API response as nested +objects with PascalCase property names, so the entire configuration is available for +inspection rather than a hand-picked subset. + +.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 response instead of the formatted object + +.EXAMPLE +Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + +.EXAMPLE +(Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password").Dns.Upstreams #> - #Work In Progress - [CmdletBinding()] + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] param ( [Parameter(Mandatory = $true)] @@ -15,6 +37,7 @@ https://ftl.pi-hole.net/master/docs/#get-/config [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) + try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl $Params = @{ @@ -31,41 +54,13 @@ https://ftl.pi-hole.net/master/docs/#get-/config Write-Output $Response } else { - $ObjectFinal = @() - $Dns = [PSCustomObject]@{ - Upstreams = $Response.config.dns.upstreams - } - - $Dhcp = [PSCustomObject]@{ - Active = $Response.config.dhcp.active - Start = $Response.config.dhcp.start - End = $Response.config.dhcp.end - Hosts = $Response.config.dhcp.hosts - IgnoreUnknownClients = $Response.config.dhcp.ignoreUnknownClients - Ipv6 = $Response.config.dhcp.ipv6 - LeaseTime = $Response.config.dhcp.leaseTime - Logging = $Response.config.dhcp.logging - MultiDNS = $Response.config.dhcp.multiDNS - Netmask = $Response.config.dhcp.netmask - RapidCommit = $Response.config.dhcp.rapidCommit - Router = $Response.config.dhcp.router - } - - $Object = [PSCustomObject]@{ - Dns = $Dns - Dhcp = $Dhcp - } - - if ($Object) { - $ObjectFinal += $Object - } - Write-Output $ObjectFinal + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.config + Write-Output $Object } } catch { Write-Error -Message $_.Exception.Message - break } finally { @@ -73,4 +68,4 @@ https://ftl.pi-hole.net/master/docs/#get-/config Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl } } -} \ No newline at end of file +} From 00cb3dc3e1c54bac4457dc553c8aa8239c8f496f Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 00:46:45 -0500 Subject: [PATCH 14/15] fix: session cleanup failures no longer fail the calling function Remove-PiHoleCurrentAuthSession is best-effort logout cleanup called from every public function's finally block. Its catch block used Write-Error, which becomes a terminating exception under $ErrorActionPreference = 'Stop' - exactly what Azure Pipelines' pwsh task sets by default. A transient network hiccup while logging out (e.g. the server briefly unreachable right after a restart, which happened mid-run on the actual CI agent) was therefore failing whichever test happened to be running at the time, even though the function under test had already succeeded. Switched to Write-Warning, which never terminates regardless of $ErrorActionPreference. Reproduced the exact failure mode locally (Write-Error throws under -Stop, Write-Warning doesn't) before and after the fix to confirm. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/Private/Misc.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PiHoleShell/Private/Misc.ps1 b/PiHoleShell/Private/Misc.ps1 index 3b28ba6..a537078 100644 --- a/PiHoleShell/Private/Misc.ps1 +++ b/PiHoleShell/Private/Misc.ps1 @@ -119,6 +119,10 @@ function Remove-PiHoleCurrentAuthSession { } catch { - Write-Error -Message $_.Exception.Message + # Best-effort logout, called from every public function's finally block - a transient + # failure here (e.g. the server briefly unreachable right after a restart) must never + # fail the caller. Write-Error would do exactly that under $ErrorActionPreference = + # 'Stop', which Azure Pipelines' pwsh task sets by default. + Write-Warning -Message "Failed to close Pi-hole session: $($_.Exception.Message)" } } \ No newline at end of file From 2aed3de2cb84d7933b07a1d320daf69c56ed14ad Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 20 Sep 2026 08:31:55 -0500 Subject: [PATCH 15/15] fix: README sync workflow can't push to a now-protected develop develop was made a protected branch requiring PRs for all changes, so this workflow's `git push origin HEAD:develop` step started failing every time it found real drift to fix (confirmed on PR #95's run: it correctly detected and committed the fix locally, then hit `GH006: Protected branch update failed... Changes must be made through a pull request` on push). Converted it to a check instead: runs the existing -Check mode and fails with instructions if the README is stale, the same way PSScriptAnalyzer already gates CI, rather than trying to auto-commit somewhere it no longer has permission to push. Also regenerated README.md itself, since the failed auto-push meant develop's copy never actually got the update - it now reflects the docstring fixes from PR #94 (Get-PiHoleConfig, Get-PiHolePadd, Get-PiHoleStatsQueryType, Get-PiHoleStatsTopDomain). Co-Authored-By: Claude Sonnet 5 --- .../workflows/SyncReadmeCommandReference.yml | 22 ++++++------------- README.md | 8 +++---- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/workflows/SyncReadmeCommandReference.yml b/.github/workflows/SyncReadmeCommandReference.yml index c409d63..3b9c3f1 100644 --- a/.github/workflows/SyncReadmeCommandReference.yml +++ b/.github/workflows/SyncReadmeCommandReference.yml @@ -6,7 +6,7 @@ on: branches: [ "main" ] permissions: - contents: write + contents: read jobs: sync-readme: @@ -20,18 +20,10 @@ jobs: ref: ${{ github.head_ref }} fetch-depth: 0 - - name: Regenerate README command reference + # develop requires PRs for all changes, so this can't auto-commit/push a fix directly to + # develop (that used to work before branch protection was added, and now fails every time + # there's real drift to fix). Instead this just fails the check with instructions, the same + # way Invoke-ScriptAnalyzer already gates PSScriptAnalyzer.yml. + - name: Check README command reference is up to date shell: pwsh - run: ./tools/Update-ReadmeCommandReference.ps1 - - - name: Commit changes if needed - run: | - if [ -n "$(git status --porcelain README.md)" ]; then - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add README.md - git commit -m "docs: sync README command reference [skip ci]" - git push origin HEAD:${{ github.head_ref }} - else - echo "README.md command reference already up to date." - fi + run: ./tools/Update-ReadmeCommandReference.ps1 -Check diff --git a/README.md b/README.md index 21fc6e6..d14d073 100644 --- a/README.md +++ b/README.md @@ -126,21 +126,21 @@ Functions marked 🚧 are still under active development — signatures and outp | `Get-PiHoleStatsDatabaseTopDomain` | Get top domains (long-term database) | | `Get-PiHoleStatsDatabaseUpstream` | Get metrics about Pi-hole's upstream destinations (long-term database) | | `Get-PiHoleStatsQuerySuggestions` | Get query filter suggestions | -| `Get-PiHoleStatsQueryType` | _No description yet_ | +| `Get-PiHoleStatsQueryType` | Get query types Request a breakdown of query types (A, AAAA, ...) | | `Get-PiHoleStatsRecentBlocked` | Request most recently blocked domain | | `Get-PiHoleStatsSummary` | Get overview of Pi-hole activity Request various query, system, and FTL properties | | `Get-PiHoleStatsTopClient` | Get top clients Request the top clients (by query count) | -| `Get-PiHoleStatsTopDomain` | _No description yet_ | +| `Get-PiHoleStatsTopDomain` | Get top domains Request the top domains (by query count) | | `Get-PiHoleStatsUpstream` | Get metrics about Pi-hole's upstream destinations | ### Configuration & Diagnostics | Function | Description | |---|---| -| `Get-PiHoleConfig` 🚧 | _No description yet_ | +| `Get-PiHoleConfig` | Get current configuration of Pi-hole | | `Get-PiHoleInfoHost` 🚧 | Get info about various host parameters This API hook returns a collection of host infos. | | `Get-PiHoleInfoMessage` | Get Pi-hole diagnosis messages Request Pi-hole diagnosis messages | -| `Get-PiHolePadd` 🚧 | _No description yet_ | +| `Get-PiHolePadd` | Get summarized data for PADD | ### Authentication