Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion PiHoleShell/PiHoleShell.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ foreach ($File in $PrivateFunctions) {

Export-ModuleMember -Function @(
#Actions
'Update-PiHoleActionsGravity', 'Invoke-PiHoleFlushNetwork', 'Restart-PiHoleDnsService' `
'Update-PiHoleActionsGravity', 'Invoke-PiHoleFlushNetwork', 'Invoke-PiHoleFlushLogs', 'Restart-PiHoleDnsService' `
#Authentication
'Remove-PiHoleCurrentAuthSession' , 'Get-PiHoleCurrentAuthSession', 'Remove-PiHoleAuthSession', `
#GroupManagement
Expand Down
71 changes: 71 additions & 0 deletions PiHoleShell/Public/Actions/Invoke-PiHoleFlushLogs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
function Invoke-PiHoleFlushLogs {
<#
.SYNOPSIS
Flushes the DNS logs

.DESCRIPTION
Flushes the Pi-hole DNS logs. This empties the DNS log file and purges the most recent 24
hours of query history from both the long-term database and FTL's internal memory.

.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
Invoke-PiHoleFlushLogs -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
#>
[CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/action/flush/logs')]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Flushes PiHole logs')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Logs matches the Pi-hole API's own endpoint name, /action/flush/logs")]
param (
[Parameter(Mandatory = $true)]
[System.URI]$PiHoleServer,
[Parameter(Mandatory = $true)]
[string]$Password,
[bool]$IgnoreSsl = $false,
[bool]$RawOutput = $false
)

try {
$Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl

$Params = @{
Headers = @{sid = $($Sid) }
Uri = "$($PiHoleServer.OriginalString)/api/action/flush/logs"
Method = "Post"
ContentType = "application/json"
SkipCertificateCheck = $IgnoreSsl
}

$Response = Invoke-RestMethod @Params

if ($RawOutput) {
Write-Output $Response
}
else {
$Object = [PSCustomObject]@{
Status = "Flushed"
}
Write-Output $Object
}
}

catch {
Write-Error -Message $_.Exception.Message
}

finally {
if ($Sid) {
Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
}
}
}
2 changes: 1 addition & 1 deletion PiHoleShell/Public/Actions/Invoke-PiHoleFlushNetwork.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function Invoke-PiHoleFlushNetwork {
Flushes the network table. This includes removing both all known devices and their associated addresses.

.DESCRIPTION
Flushes the Pi-hole log file (/var/log/pihole/pihole.log).
Flushes Pi-hole's network table, removing all known devices and their associated addresses.

.PARAMETER PiHoleServer
The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Functions marked 🚧 are still under active development — signatures and outp

| Function | Description |
|---|---|
| `Invoke-PiHoleFlushLogs` | Flushes the DNS logs |
| `Invoke-PiHoleFlushNetwork` | Flushes the network table. This includes removing both all known devices and their associated addresses. |
| `Restart-PiHoleDnsService` | Restarts the pihole-FTL service |
| `Update-PiHoleActionsGravity` | Update Pi-hole's adlists by running pihole -g |
Expand Down
76 changes: 75 additions & 1 deletion azuredevops-pihole-pester-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,88 @@ steps:
- pwsh: |
Import-Module Pester -MinimumVersion 5.0.0 -Force

# On a PR build, scope the run to tests for the files that actually changed - tests/ mirrors
# PiHoleShell/Public/<Category>/ 1:1, so a changed function maps directly to its test file(s).
# Any ambiguity (shared/private code, module manifest, shared test helpers, or the diff itself
# failing) falls back to the full suite rather than risk silently skipping real coverage.
# Direct pushes to develop/main (not PR builds) always run the full suite.
$runFullSuite = $true
$testPaths = [System.Collections.Generic.List[string]]::new()

if ($env:BUILD_REASON -eq 'PullRequest') {
try {
# The default checkout is shallow, which leaves no common ancestor with the target
# branch to diff against ("no merge base") - unshallow first so the diff can work.
if ((git rev-parse --is-shallow-repository) -eq 'true') {
git fetch --unshallow --quiet
if ($LASTEXITCODE -ne 0) { throw "git fetch --unshallow failed" }
}

# Fetch with an explicit refspec so origin/<branch> is created even if the
# checkout's default fetch refspec is restricted to a single branch.
$targetBranch = $env:SYSTEM_PULLREQUEST_TARGETBRANCH -replace '^refs/heads/', ''
git fetch origin "${targetBranch}:refs/remotes/origin/${targetBranch}" --quiet
if ($LASTEXITCODE -ne 0) { throw "git fetch failed for '$targetBranch'" }

$changedFiles = git diff --name-only "origin/$targetBranch...HEAD"
if ($LASTEXITCODE -ne 0) { throw "git diff failed against 'origin/$targetBranch'" }

if ($changedFiles) {
$runFullSuite = $false

foreach ($file in $changedFiles) {
if ($runFullSuite) { break }

if ($file -match '^PiHoleShell/Private/' -or
$file -eq 'PiHoleShell/PiHoleShell.psm1' -or
$file -eq 'PiHoleShell/PiHoleShell.psd1' -or
$file -eq 'PiHoleShell/Public/Authentication/Request-PiHoleAuth.ps1' -or
$file -eq 'tests/Initialize-PiHoleTestData.ps1' -or
$file -eq 'tests/IntegrationConfig.example.ps1') {
Write-Host "Shared/core file changed ($file) - running the full suite"
$runFullSuite = $true
}
elseif ($file -match '^PiHoleShell/Public/([^/]+)/([^/]+)\.ps1$') {
$category = $Matches[1]
$function = $Matches[2]
$testDir = "tests/$category"
if (Test-Path $testDir) {
Get-ChildItem -Path $testDir -Filter "$function.*Tests.ps1" | ForEach-Object {
$testPaths.Add($_.FullName)
}
}
}
elseif ($file -match '^tests/.*\.Tests\.ps1$' -and (Test-Path $file)) {
$testPaths.Add((Resolve-Path $file).Path)
}
# Anything else (README, workflows, tools/, etc.) has no test impact - ignored.
}
}
}
catch {
Write-Warning "Could not compute changed-file test scope, falling back to the full suite: $($_.Exception.Message)"
$runFullSuite = $true
}
}

$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Run.Exit = $true
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'NUnitXml'
$config.TestResult.OutputPath = '$(testResultsFile)'

$testPaths = $testPaths | Select-Object -Unique
if ($runFullSuite -or $testPaths.Count -eq 0) {
Write-Host "Running the full test suite"
$config.Run.Path = './tests'
}
else {
Write-Host "Running scoped tests for changed files:"
$testPaths | ForEach-Object { Write-Host " $_" }
$config.Run.Path = $testPaths
}

Invoke-Pester -Configuration $config
displayName: 'Run Pester tests'
workingDirectory: '$(System.DefaultWorkingDirectory)'
Expand Down
46 changes: 46 additions & 0 deletions tests/Actions/Invoke-PiHoleFlushLogs.Integration.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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.

# Config availability must be known at discovery time so the -Skip parameter on each It block
# (evaluated during discovery, before BeforeAll runs) sees the correct value.
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Invoke-PiHoleFlushLogs (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

# Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because
# Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value.
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
$script:PiHoleToken = $PiHoleToken
$script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
}
}

It 'flushes the DNS logs and returns a formatted status' -Skip:(-not $script:ConfigAvailable) {
$result = Invoke-PiHoleFlushLogs -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) {
$result = Invoke-PiHoleFlushLogs -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
$result | Format-List | Out-String | Write-Host

$result.status | Should -Be 'success'
}

It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
$result = Invoke-PiHoleFlushLogs -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue

$errOut | Should -Not -BeNullOrEmpty
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

# Config availability must be known at discovery time so the -Skip parameter on each It block
# (evaluated during discovery, before BeforeAll runs) sees the correct value.
$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Invoke-PiHoleFlushNetwork (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

# Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because
# Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value.
$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

# Config availability must be known at discovery time so the -Skip parameter on each It block
# (evaluated during discovery, before BeforeAll runs) sees the correct value.
$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Restart-PiHoleDnsService (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

# Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because
# Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value.
$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

# Config availability must be known at discovery time so the -Skip parameter on each It block
# (evaluated during discovery, before BeforeAll runs) sees the correct value.
$script:ConfigAvailable = Test-Path (Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Update-PiHoleActionsGravity (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

# Recomputed here (not read from the discovery-time $script:ConfigAvailable above) because
# Pester runs discovery and run in separate scopes, so BeforeAll cannot see that value.
$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# (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')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Get-PiHoleCurrentAuthSession (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# (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')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Get-PiHoleConfig (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# (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')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Get-PiHoleDnsBlockingStatus (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# (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')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Get-PiHoleInfoHost (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# (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')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Get-PiHoleInfoMessage (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
# (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')
$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')

Describe 'Get-PiHoleGroup (Integration)' -Tag 'Integration' {
BeforeAll {
Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force

$configPath = Join-Path $PSScriptRoot 'IntegrationConfig.local.ps1'
$configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
if (Test-Path $configPath) {
. $configPath
$script:PiHoleServer = $PiHoleServer
Expand Down
Loading
Loading