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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,14 @@ jobs:
}

[xml]$doc = Get-Content -LiteralPath $coverageXmlPath
$lineCounter = $doc.SelectNodes('//counter') | Where-Object { $_.GetAttribute('type') -eq 'LINE' } | Select-Object -First 1
# Must be the <counter> that is a direct child of the root <report> element -
# the report-level aggregate. A plain "//counter" search matches every nested
# per-package/per-sourcefile/per-class/per-method counter too, and picking the
# first LINE-type match in document order silently reports whichever function
# happens to appear first in the report instead of the overall total.
$lineCounter = $doc.SelectSingleNode('/report/counter[@type="LINE"]')
if (-not $lineCounter) {
throw 'No LINE counter found in coverage XML.'
throw 'No report-level LINE counter found in coverage XML.'
}

$missed = [int]$lineCounter.GetAttribute('missed')
Expand Down
111 changes: 111 additions & 0 deletions tests/Unit/NetClean.Core.Unit.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,96 @@ Describe 'NetClean core/shared helper unit tests' {
$result.JoinType | Should -Be 'DomainJoined'
@($result.Warnings).Count | Should -BeGreaterThan 0
}

It 'warns and continues when the dsregcmd capture itself throws' {
Mock Invoke-NetCleanNativeCapture { throw 'dsregcmd.exe not found' }
Mock Get-CimInstance {
[pscustomobject]@{ PartOfDomain = $false }
} -ParameterFilter { $ClassName -eq 'Win32_ComputerSystem' }
Mock Get-ScheduledTask { @() }

$result = Get-NetCleanDeviceManagementState

$result.JoinType | Should -Be 'Workgroup'
@($result.Warnings) | Should -Contain 'Microsoft Entra join-state detection was unavailable.'
}

It 'warns and continues when both dsregcmd and the domain fallback are unavailable' {
Mock Invoke-NetCleanNativeCapture {
[pscustomobject]@{ Succeeded = $false; Output = @(); Error = 'dsregcmd failed' }
}
Mock Get-CimInstance { throw 'WMI unavailable' } -ParameterFilter { $ClassName -eq 'Win32_ComputerSystem' }
Mock Get-ScheduledTask { @() }

$result = Get-NetCleanDeviceManagementState

$result.IsManaged | Should -BeFalse
$result.JoinType | Should -Be 'Workgroup'
@($result.Warnings) | Should -Contain 'Active Directory domain-state fallback was unavailable.'
}

It 'warns and continues when MDM enrollment-task detection throws' {
Mock Invoke-NetCleanNativeCapture {
[pscustomobject]@{
Succeeded = $true
Output = @(
' AzureAdJoined : NO',
' DomainJoined : NO',
' EnterpriseJoined : NO',
' WorkplaceJoined : NO'
)
Error = $null
}
}
Mock Get-ScheduledTask { throw 'access denied' }

$result = Get-NetCleanDeviceManagementState

$result.MdmEnrolled | Should -BeFalse
@($result.Warnings) | Should -Contain 'MDM enrollment-task detection was unavailable.'
}

It 'classifies an Entra-only join (not hybrid) with the Entra join type' {
Mock Invoke-NetCleanNativeCapture {
[pscustomobject]@{
Succeeded = $true
Output = @(
' AzureAdJoined : YES',
' DomainJoined : NO',
' EnterpriseJoined : NO',
' WorkplaceJoined : NO'
)
Error = $null
}
}
Mock Get-ScheduledTask { @() }

$result = Get-NetCleanDeviceManagementState

$result.IsManaged | Should -BeTrue
$result.JoinType | Should -Be 'MicrosoftEntraJoined'
}

It 'classifies an enterprise-joined device with the enterprise join type' {
Mock Invoke-NetCleanNativeCapture {
[pscustomobject]@{
Succeeded = $true
Output = @(
' AzureAdJoined : NO',
' DomainJoined : NO',
' EnterpriseJoined : YES',
' WorkplaceJoined : NO'
)
Error = $null
}
}
Mock Get-ScheduledTask { @() }

$result = Get-NetCleanDeviceManagementState

$result.IsManaged | Should -BeTrue
$result.JoinType | Should -Be 'EnterpriseJoined'
}
}

Context 'Invoke-InParallel' {
Expand Down Expand Up @@ -376,6 +466,27 @@ Describe 'NetClean core/shared helper unit tests' {
Invoke-InParallel -ScriptBlock { param($item) $item } -InputObjects @(1) -ThrottleLimit 0
} | Should -Throw
}

It 'runs a single input item without a runspace pool' {
$result = @(
Invoke-InParallel -ScriptBlock {
param($item)
$item * 2
} -InputObjects @(5)
)

$result | Should -Be @(10)
}

It 'returns an empty collection when the single input item throws' {
$result = @(
Invoke-InParallel -ScriptBlock {
throw 'single worker failed'
} -InputObjects @(1)
)

$result.Count | Should -Be 0
}
}

Context 'New-DirectoryIfNotExist' {
Expand Down
11 changes: 11 additions & 0 deletions tests/Unit/NetClean.Coverage.Unit.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ Describe 'NetClean coverage runner' {
$script:ciText | Should -Match 'min-coverage-overall:\s+94'
}

It 'computes the coverage badge percentage from the report-level aggregate counter' {
# A bare "//counter" XPath match's every nested per-package/per-sourcefile/
# per-class/per-method LINE counter too, not just the overall total - taking
# the first match in document order silently reports whichever function
# happens to appear first in the report (observed live: 46.43% instead of
# the real ~96%). The counter must be selected as a direct child of the
# root <report> element.
$script:ciText | Should -Match '\$doc\.SelectSingleNode\(''/report/counter\[@type="LINE"\]''\)'
$script:ciText | Should -Not -Match '\$doc\.SelectNodes\(''//counter''\)'
}

It 'pins workflow actions and Pages deployment steps' {
$script:ciText | Should -Match 'actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8\s+# v6\.0\.1'
$script:ciText | Should -Match 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a\s+# v7\.0\.1'
Expand Down
Loading